diff --git a/cspell.yaml b/cspell.yaml index fbd0db71..b0f34aa6 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -146,6 +146,7 @@ words: - sloerrorbudgetstatus - sloname - slos + - slostatusapi - sourcetype - stablebetaalpha - startuml diff --git a/sdk/client.go b/sdk/client.go index 8e8c96e3..654750f1 100644 --- a/sdk/client.go +++ b/sdk/client.go @@ -16,6 +16,7 @@ import ( "github.com/nobl9/nobl9-go/manifest" "github.com/nobl9/nobl9-go/sdk/endpoints/authdata" "github.com/nobl9/nobl9-go/sdk/endpoints/objects" + "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi" ) // ProjectsWildcard is used in HeaderProject when requesting for all projects. @@ -88,6 +89,10 @@ func (c *Client) AuthData() authdata.Versions { return authdata.NewVersions(c) } +func (c *Client) SLOStatusAPI() slostatusapi.Versions { + return slostatusapi.NewVersions(c) +} + // CreateRequest creates a new http.Request pointing at the Nobl9 API URL. // It also adds all the mandatory headers to the request and encodes query parameters. func (c *Client) CreateRequest( diff --git a/sdk/endpoints/slostatusapi/doc.go b/sdk/endpoints/slostatusapi/doc.go new file mode 100644 index 00000000..7a5e4058 --- /dev/null +++ b/sdk/endpoints/slostatusapi/doc.go @@ -0,0 +1,2 @@ +// Package slostatusapi provides the API for managing SLO status API requests in the SDK. +package slostatusapi diff --git a/sdk/endpoints/slostatusapi/v1/doc.go b/sdk/endpoints/slostatusapi/v1/doc.go new file mode 100644 index 00000000..383003d7 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/doc.go @@ -0,0 +1,2 @@ +// Package v1 provides the API for managing SLO status API V1 requests in the SDK. +package v1 diff --git a/sdk/endpoints/slostatusapi/v1/endpoints.go b/sdk/endpoints/slostatusapi/v1/endpoints.go new file mode 100644 index 00000000..61f8b98c --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/endpoints.go @@ -0,0 +1,87 @@ +package v1 + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "path" + "strconv" + + "github.com/pkg/errors" + + endpointsHelpers "github.com/nobl9/nobl9-go/internal/endpoints" + "github.com/nobl9/nobl9-go/internal/sdk" +) + +const ( + baseAPIPath = "v1/slos" +) + +//go:generate ../../../../bin/ifacemaker -y " " -f ./*.go -s endpoints -i Endpoints -o endpoints_interface.go -p "$GOPACKAGE" + +func NewEndpoints(client endpointsHelpers.Client) Endpoints { + return endpoints{client: client} +} + +type endpoints struct { + client endpointsHelpers.Client +} + +func (e endpoints) GetSLO(ctx context.Context, project, name string) (slo SLODetails, err error) { + req, err := e.client.CreateRequest( + ctx, + http.MethodGet, + path.Join(baseAPIPath, name), + http.Header{sdk.HeaderProject: {project}}, + nil, + nil, + ) + if err != nil { + return slo, err + } + resp, err := e.client.Do(req) + if err != nil { + return slo, err + } + defer func() { _ = resp.Body.Close() }() + + if err = json.NewDecoder(resp.Body).Decode(&slo); err != nil { + return slo, errors.Wrap(err, "failed to decode response body") + } + return slo, nil +} + +func (e endpoints) GetSLOs(ctx context.Context, params GetSLOsRequest) (slos SLOListResponse, err error) { + req, err := e.client.CreateRequest( + ctx, + http.MethodGet, + baseAPIPath, + nil, + url.Values{ + QueryKeyLimit: []string{strconv.Itoa(params.Limit)}, + QueryKeyCursor: []string{params.Cursor}, + }, + nil, + ) + if err != nil { + return slos, err + } + resp, err := e.client.Do(req) + if err != nil { + return slos, err + } + defer func() { _ = resp.Body.Close() }() + + if err = json.NewDecoder(resp.Body).Decode(&slos); err != nil { + return slos, errors.Wrap(err, "failed to decode response body") + } + if slos.Links.Next != "" { + nextURL, err := url.Parse(slos.Links.Next) + if err != nil { + return slos, errors.Wrap(err, "failed to parse 'next' cursor link URL") + } + slos.Links.Cursor = nextURL.Query().Get("cursor") + } + return slos, nil +} diff --git a/sdk/endpoints/slostatusapi/v1/endpoints_interface.go b/sdk/endpoints/slostatusapi/v1/endpoints_interface.go new file mode 100644 index 00000000..5e2cd4e4 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/endpoints_interface.go @@ -0,0 +1,12 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package v1 + +import ( + "context" +) + +type Endpoints interface { + GetSLO(ctx context.Context, project, name string) (slo SLODetails, err error) + GetSLOs(ctx context.Context, params GetSLOsRequest) (slos SLOListResponse, err error) +} diff --git a/sdk/endpoints/slostatusapi/v1/query.go b/sdk/endpoints/slostatusapi/v1/query.go new file mode 100644 index 00000000..f68356a1 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/query.go @@ -0,0 +1,6 @@ +package v1 + +const ( + QueryKeyLimit = "limit" + QueryKeyCursor = "cursor" +) diff --git a/sdk/endpoints/slostatusapi/v1/request.go b/sdk/endpoints/slostatusapi/v1/request.go new file mode 100644 index 00000000..ee8a26bd --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/request.go @@ -0,0 +1,6 @@ +package v1 + +type GetSLOsRequest struct { + Limit int + Cursor string +} diff --git a/sdk/endpoints/slostatusapi/v1/response.go b/sdk/endpoints/slostatusapi/v1/response.go new file mode 100644 index 00000000..75add5ba --- /dev/null +++ b/sdk/endpoints/slostatusapi/v1/response.go @@ -0,0 +1,61 @@ +package v1 + +type SLOListResponse struct { + Data []SLODetails `json:"data"` + Links Links `json:"links"` +} + +type Links struct { + Self string `json:"self"` + Next string `json:"next"` + Cursor string `json:"cursor"` +} + +type SLODetails struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Project string `json:"project"` + Service string `json:"service"` + CreatedAt string `json:"createdAt"` + Objectives []Objective `json:"objectives"` + Composite *Composite `json:"composite,omitempty"` + Labels map[string][]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + BudgetingMethod string `json:"budgetingMethod"` +} + +type Objective struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Target float64 `json:"target"` + BurnRate *float64 `json:"burnRate,omitempty"` + ErrorBudgetRemaining *float64 `json:"errorBudgetRemaining,omitempty"` + ErrorBudgetRemainingPercentage *float64 `json:"errorBudgetRemainingPercentage,omitempty"` + Reliability *float64 `json:"reliability,omitempty"` + Counts *Counts `json:"counts,omitempty"` + SLIType string `json:"sliType"` +} + +type Composite struct { + Target *float64 `json:"target,omitempty"` + BurnRateCondition *BurnRateCondition `json:"burnRateCondition,omitempty"` + CompositeObjective +} + +type CompositeObjective struct { + BurnRate *float64 `json:"burnRate,omitempty"` + ErrorBudgetRemaining *float64 `json:"errorBudgetRemaining,omitempty"` + ErrorBudgetRemainingPercentage *float64 `json:"errorBudgetRemainingPercentage,omitempty"` + Reliability *float64 `json:"reliability,omitempty"` +} + +type BurnRateCondition struct { + Value float64 `json:"value"` + Operator string `json:"op"` +} + +type Counts struct { + Good *float64 `json:"good,omitempty"` + Total *float64 `json:"total,omitempty"` +} diff --git a/sdk/endpoints/slostatusapi/v2/doc.go b/sdk/endpoints/slostatusapi/v2/doc.go new file mode 100644 index 00000000..be771cc8 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/doc.go @@ -0,0 +1,2 @@ +// Package v2 provides the API for managing SLO status API V2 requests in the SDK. +package v2 diff --git a/sdk/endpoints/slostatusapi/v2/endpoints.go b/sdk/endpoints/slostatusapi/v2/endpoints.go new file mode 100644 index 00000000..8e1c0829 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/endpoints.go @@ -0,0 +1,87 @@ +package v2 + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "path" + "strconv" + + "github.com/pkg/errors" + + endpointsHelpers "github.com/nobl9/nobl9-go/internal/endpoints" + "github.com/nobl9/nobl9-go/internal/sdk" +) + +const ( + baseAPIPath = "v2/slos" +) + +//go:generate ../../../../bin/ifacemaker -y " " -f ./*.go -s endpoints -i Endpoints -o endpoints_interface.go -p "$GOPACKAGE" + +func NewEndpoints(client endpointsHelpers.Client) Endpoints { + return endpoints{client: client} +} + +type endpoints struct { + client endpointsHelpers.Client +} + +func (e endpoints) GetSLO(ctx context.Context, project, name string) (slo SLODetails, err error) { + req, err := e.client.CreateRequest( + ctx, + http.MethodGet, + path.Join(baseAPIPath, name), + http.Header{sdk.HeaderProject: {project}}, + nil, + nil, + ) + if err != nil { + return slo, err + } + resp, err := e.client.Do(req) + if err != nil { + return slo, err + } + defer func() { _ = resp.Body.Close() }() + + if err = json.NewDecoder(resp.Body).Decode(&slo); err != nil { + return slo, errors.Wrap(err, "failed to decode response body") + } + return slo, nil +} + +func (e endpoints) GetSLOs(ctx context.Context, params GetSLOsRequest) (slos SLOListResponse, err error) { + req, err := e.client.CreateRequest( + ctx, + http.MethodGet, + baseAPIPath, + nil, + url.Values{ + QueryKeyLimit: []string{strconv.Itoa(params.Limit)}, + QueryKeyCursor: []string{params.Cursor}, + }, + nil, + ) + if err != nil { + return slos, err + } + resp, err := e.client.Do(req) + if err != nil { + return slos, err + } + defer func() { _ = resp.Body.Close() }() + + if err = json.NewDecoder(resp.Body).Decode(&slos); err != nil { + return slos, errors.Wrap(err, "failed to decode response body") + } + if slos.Links.Next != "" { + nextURL, err := url.Parse(slos.Links.Next) + if err != nil { + return slos, errors.Wrap(err, "failed to parse 'next' cursor link URL") + } + slos.Links.Cursor = nextURL.Query().Get("cursor") + } + return slos, nil +} diff --git a/sdk/endpoints/slostatusapi/v2/endpoints_interface.go b/sdk/endpoints/slostatusapi/v2/endpoints_interface.go new file mode 100644 index 00000000..c460af9d --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/endpoints_interface.go @@ -0,0 +1,12 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package v2 + +import ( + "context" +) + +type Endpoints interface { + GetSLO(ctx context.Context, project, name string) (slo SLODetails, err error) + GetSLOs(ctx context.Context, params GetSLOsRequest) (slos SLOListResponse, err error) +} diff --git a/sdk/endpoints/slostatusapi/v2/query.go b/sdk/endpoints/slostatusapi/v2/query.go new file mode 100644 index 00000000..89cbbbe4 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/query.go @@ -0,0 +1,6 @@ +package v2 + +const ( + QueryKeyLimit = "limit" + QueryKeyCursor = "cursor" +) diff --git a/sdk/endpoints/slostatusapi/v2/request.go b/sdk/endpoints/slostatusapi/v2/request.go new file mode 100644 index 00000000..c59c869e --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/request.go @@ -0,0 +1,6 @@ +package v2 + +type GetSLOsRequest struct { + Limit int + Cursor string +} diff --git a/sdk/endpoints/slostatusapi/v2/response.go b/sdk/endpoints/slostatusapi/v2/response.go new file mode 100644 index 00000000..9e914a08 --- /dev/null +++ b/sdk/endpoints/slostatusapi/v2/response.go @@ -0,0 +1,73 @@ +package v2 + +type SLOListResponse struct { + Data []SLODetails `json:"data"` + Links Links `json:"links"` +} + +type Links struct { + Self string `json:"self"` + Next string `json:"next"` + Cursor string `json:"cursor"` +} + +type SLODetails struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Project Project `json:"project"` + Service Service `json:"service"` + CreatedAt string `json:"createdAt"` + Objectives []Objective `json:"objectives"` + Composite *Composite `json:"composite,omitempty"` + Labels map[string][]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + BudgetingMethod string `json:"budgetingMethod"` +} + +type Project struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Labels map[string][]string `json:"labels,omitempty"` +} + +type Service struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Labels map[string][]string `json:"labels,omitempty"` +} + +type Objective struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Target float64 `json:"target"` + BurnRate *float64 `json:"burnRate,omitempty"` + ErrorBudgetRemaining *float64 `json:"errorBudgetRemaining,omitempty"` + ErrorBudgetRemainingPercentage *float64 `json:"errorBudgetRemainingPercentage,omitempty"` + Reliability *float64 `json:"reliability,omitempty"` + Counts *Counts `json:"counts,omitempty"` + SLIType string `json:"sliType"` +} + +type Composite struct { + Target *float64 `json:"target,omitempty"` + BurnRateCondition *BurnRateCondition `json:"burnRateCondition,omitempty"` + CompositeObjective +} + +type CompositeObjective struct { + BurnRate *float64 `json:"burnRate,omitempty"` + ErrorBudgetRemaining *float64 `json:"errorBudgetRemaining,omitempty"` + ErrorBudgetRemainingPercentage *float64 `json:"errorBudgetRemainingPercentage,omitempty"` + Reliability *float64 `json:"reliability,omitempty"` +} + +type BurnRateCondition struct { + Value float64 `json:"value"` + Operator string `json:"op"` +} + +type Counts struct { + Good *float64 `json:"good,omitempty"` + Total *float64 `json:"total,omitempty"` +} diff --git a/sdk/endpoints/slostatusapi/versions.go b/sdk/endpoints/slostatusapi/versions.go new file mode 100644 index 00000000..1553f5d5 --- /dev/null +++ b/sdk/endpoints/slostatusapi/versions.go @@ -0,0 +1,25 @@ +package slostatusapi + +import ( + "github.com/nobl9/nobl9-go/internal/endpoints" + v1 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v1" + v2 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v2" +) + +//go:generate ../../../bin/ifacemaker -y " " -f ./*.go -s versions -i Versions -o versions_interface.go -p "$GOPACKAGE" + +func NewVersions(client endpoints.Client) Versions { + return versions{client: client} +} + +type versions struct { + client endpoints.Client +} + +func (v versions) V1() v1.Endpoints { + return v1.NewEndpoints(v.client) +} + +func (v versions) V2() v2.Endpoints { + return v2.NewEndpoints(v.client) +} diff --git a/sdk/endpoints/slostatusapi/versions_interface.go b/sdk/endpoints/slostatusapi/versions_interface.go new file mode 100644 index 00000000..6a349aab --- /dev/null +++ b/sdk/endpoints/slostatusapi/versions_interface.go @@ -0,0 +1,13 @@ +// Code generated by ifacemaker; DO NOT EDIT. + +package slostatusapi + +import ( + v1 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v1" + v2 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v2" +) + +type Versions interface { + V1() v1.Endpoints + V2() v2.Endpoints +} diff --git a/tests/slostatusapi_test.go b/tests/slostatusapi_test.go new file mode 100644 index 00000000..4dfb7a59 --- /dev/null +++ b/tests/slostatusapi_test.go @@ -0,0 +1,243 @@ +//go:build e2e_test + +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + v1alphaExamples "github.com/nobl9/nobl9-go/internal/manifest/v1alpha/examples" + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha" + v1alphaDirect "github.com/nobl9/nobl9-go/manifest/v1alpha/direct" + v1alphaService "github.com/nobl9/nobl9-go/manifest/v1alpha/service" + v1alphaSLO "github.com/nobl9/nobl9-go/manifest/v1alpha/slo" + v1 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v1" + v2 "github.com/nobl9/nobl9-go/sdk/endpoints/slostatusapi/v2" +) + +func Test_SLOStatusAPI_V1_GetSLO(t *testing.T) { + t.Parallel() + ctx := context.Background() + + allObjects := setupSLOListTest(t) + project, _, slo := allObjects[0], allObjects[1], allObjects[2] + v1Apply(t, allObjects) + t.Cleanup(func() { v1Delete(t, allObjects) }) + + responseSLO, err := tryExecuteRequest(t, func() (v1.SLODetails, error) { + return client.SLOStatusAPI().V1().GetSLO(ctx, project.GetName(), slo.GetName()) + }) + require.NoError(t, err) + assert.NotEmpty(t, responseSLO) + assert.Equal(t, slo.GetName(), responseSLO.Name) +} + +func Test_SLOStatusAPI_V1_GetSLOs(t *testing.T) { + t.Parallel() + ctx := context.Background() + + initialObjects := setupSLOListTest(t) + _, _, slo := initialObjects[0], initialObjects[1], initialObjects[2] + slo1 := slo.(*v1alphaSLO.SLO) + slo2 := deepCopyObject(t, slo1) + slo2.Metadata.Name = generateName() + initialObjects = append(initialObjects, slo2) + v1Apply(t, initialObjects) + + slo3 := deepCopyObject(t, slo1) + slo3.Metadata.Name = generateName() + slo4 := deepCopyObject(t, slo1) + slo4.Metadata.Name = generateName() + v1Apply(t, []manifest.Object{slo3, slo4}) + + slo5 := deepCopyObject(t, slo1) + slo5.Metadata.Name = generateName() + v1Apply(t, []manifest.Object{slo5}) + + t.Cleanup(func() { v1Delete(t, initialObjects) }) + t.Cleanup(func() { v1Delete(t, []manifest.Object{slo3, slo4, slo5}) }) + + limit := 2 + firstResponse, err := tryExecuteRequest(t, func() (v1.SLOListResponse, error) { + response, err := client.SLOStatusAPI().V1().GetSLOs(ctx, v1.GetSLOsRequest{Limit: limit}) + if err != nil { + return response, err + } + if len(response.Data) != limit { + err = fmt.Errorf("expected %d SLOs, got %d", limit, len(response.Data)) + } + return response, err + }) + require.NoError(t, err) + assert.NotEmpty(t, firstResponse) + assert.NotEmpty(t, firstResponse.Links.Self, "expected first response's self link to be set") + assert.NotEmpty(t, firstResponse.Links.Next, "expected first response's next link to be set") + firstCursor := firstResponse.Links.Cursor + require.NotEmpty(t, firstCursor) + + secondResponse, err := tryExecuteRequest(t, func() (v1.SLOListResponse, error) { + response, err := client.SLOStatusAPI().V1().GetSLOs(ctx, v1.GetSLOsRequest{Limit: limit, Cursor: firstCursor}) + if err != nil { + return response, err + } + if len(response.Data) != limit { + err = fmt.Errorf("expected %d SLOs, got %d", limit, len(response.Data)) + } + return response, err + }) + require.NoError(t, err) + assert.NotEmpty(t, secondResponse) + assert.NotEmpty(t, secondResponse.Links.Self, "expected second response's self link to be set") + assert.NotEmpty(t, secondResponse.Links.Next, "expected second response's next link to be set") + secondCursor := secondResponse.Links.Cursor + require.NotEmpty(t, secondCursor) + assert.NotEqual(t, firstCursor, secondCursor) + assert.NotEqual(t, firstResponse, secondResponse) +} + +func Test_SLOStatusAPI_V2_GetSLO(t *testing.T) { + t.Parallel() + ctx := context.Background() + + allObjects := setupSLOListTest(t) + project, _, slo := allObjects[0], allObjects[1], allObjects[2] + v1Apply(t, allObjects) + t.Cleanup(func() { v1Delete(t, allObjects) }) + + responseSLO, err := tryExecuteRequest(t, func() (v2.SLODetails, error) { + return client.SLOStatusAPI().V2().GetSLO(ctx, project.GetName(), slo.GetName()) + }) + require.NoError(t, err) + assert.NotEmpty(t, responseSLO) + assert.Equal(t, slo.GetName(), responseSLO.Name) +} + +func Test_SLOStatusAPI_V2_GetSLOs(t *testing.T) { + t.Parallel() + ctx := context.Background() + + initialObjects := setupSLOListTest(t) + _, _, slo := initialObjects[0], initialObjects[1], initialObjects[2] + slo1 := slo.(*v1alphaSLO.SLO) + slo2 := deepCopyObject(t, slo1) + slo2.Metadata.Name = generateName() + initialObjects = append(initialObjects, slo2) + v1Apply(t, initialObjects) + + slo3 := deepCopyObject(t, slo1) + slo3.Metadata.Name = generateName() + slo4 := deepCopyObject(t, slo1) + slo4.Metadata.Name = generateName() + v1Apply(t, []manifest.Object{slo3, slo4}) + + slo5 := deepCopyObject(t, slo1) + slo5.Metadata.Name = generateName() + v1Apply(t, []manifest.Object{slo5}) + + t.Cleanup(func() { v1Delete(t, initialObjects) }) + t.Cleanup(func() { v1Delete(t, []manifest.Object{slo3, slo4, slo5}) }) + + limit := 2 + firstResponse, err := tryExecuteRequest(t, func() (v2.SLOListResponse, error) { + response, err := client.SLOStatusAPI().V2().GetSLOs(ctx, v2.GetSLOsRequest{Limit: limit}) + if err != nil { + return response, err + } + if len(response.Data) != limit { + err = fmt.Errorf("expected %d SLOs, got %d", limit, len(response.Data)) + } + return response, err + }) + require.NoError(t, err) + assert.NotEmpty(t, firstResponse) + assert.NotEmpty(t, firstResponse.Links.Self, "expected first response's self link to be set") + assert.NotEmpty(t, firstResponse.Links.Next, "expected first response's next link to be set") + firstCursor := firstResponse.Links.Cursor + require.NotEmpty(t, firstCursor) + + secondResponse, err := tryExecuteRequest(t, func() (v2.SLOListResponse, error) { + response, err := client.SLOStatusAPI().V2().GetSLOs(ctx, v2.GetSLOsRequest{Limit: limit, Cursor: firstCursor}) + if err != nil { + return response, err + } + if len(response.Data) != limit { + err = fmt.Errorf("expected %d SLOs, got %d", limit, len(response.Data)) + } + return response, err + }) + require.NoError(t, err) + assert.NotEmpty(t, secondResponse) + assert.NotEmpty(t, secondResponse.Links.Self, "expected second response's self link to be set") + assert.NotEmpty(t, secondResponse.Links.Next, "expected second response's next link to be set") + secondCursor := secondResponse.Links.Cursor + require.NotEmpty(t, secondCursor) + assert.NotEqual(t, firstCursor, secondCursor) + assert.NotEqual(t, firstResponse, secondResponse) +} + +func setupSLOListTest(t *testing.T) []manifest.Object { + t.Helper() + project := generateV1alphaProject(t) + service := newV1alphaService(t, v1alphaService.Metadata{ + Name: generateName(), + Project: project.GetName(), + }) + + dataSourceType := v1alpha.Datadog + directs := filterSlice(v1alphaSLODependencyDirects(t), func(o manifest.Object) bool { + typ, _ := o.(v1alphaDirect.Direct).Spec.GetType() + return typ == dataSourceType + }) + require.Len(t, directs, 1) + direct := directs[0].(v1alphaDirect.Direct) + + slo := getExample[v1alphaSLO.SLO](t, + manifest.KindSLO, + func(example v1alphaExamples.Example) bool { + dsGetter, ok := example.(dataSourceTypeGetter) + return ok && dsGetter.GetDataSourceType() == dataSourceType + }, + ) + slo.Spec.AnomalyConfig = nil + slo.Metadata.Name = generateName() + slo.Metadata.Project = project.GetName() + slo.Spec.Indicator.MetricSource = v1alphaSLO.MetricSourceSpec{ + Name: direct.Metadata.Name, + Project: direct.Metadata.Project, + Kind: manifest.KindDirect, + } + slo.Spec.AlertPolicies = nil + slo.Spec.Service = service.Metadata.Name + slo.Spec.Objectives[0].Name = "good" + return []manifest.Object{project, service, slo} +} + +func tryExecuteRequest[T any](t *testing.T, reqFunc func() (T, error)) (T, error) { + t.Helper() + ticker := time.NewTicker(5 * time.Second) + timer := time.NewTimer(time.Minute) + defer ticker.Stop() + defer timer.Stop() + var ( + response T + err error + ) + for { + select { + case <-ticker.C: + response, err = reqFunc() + if err == nil { + return response, nil + } + case <-timer.C: + t.Error("timeout") + return response, err + } + } +}