Files
Florian BacherandSanskar Jaiswal ce976e28f0 feat: implement a Keptn metrics provider
Add a Keptn metrics provider for two resources:
* KeptnMetric: Verify the value of a single metric.
* Analysis (via AnalysisDefinition): Run a Keptn analysis over an
  interval validating SLOs.

Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>
2024-06-11 19:23:03 +05:30

382 lines
9.8 KiB
Go

package providers
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
)
func TestNewKeptnProvider(t *testing.T) {
provider, err := NewKeptnProvider(&rest.Config{})
require.Nil(t, err)
require.NotNil(t, provider)
isOnline, err := provider.IsOnline()
require.NoError(t, err)
require.True(t, isOnline)
}
func TestNewKeptnProvider_NoKubeConfig(t *testing.T) {
provider, err := NewKeptnProvider(nil)
require.Error(t, err)
require.Nil(t, provider)
}
func TestKeptnProvider_RunQuery_KeptnMetric(t *testing.T) {
tests := []struct {
name string
setupClient func() *fake.FakeDynamicClient
query string
want float64
wantErr bool
}{
{
name: "wrong query format",
setupClient: func() *fake.FakeDynamicClient {
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
getSampleKeptnMetric("my-metric", "3.0"),
)
return fakeClient
},
query: "invalid/default",
want: 0,
wantErr: true,
},
{
name: "unsupported resource type",
setupClient: func() *fake.FakeDynamicClient {
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
getSampleKeptnMetric("my-metric", "3.0"),
)
return fakeClient
},
query: "invalid/default/my-metric",
want: 0,
wantErr: true,
},
{
name: "get KeptnMetric value",
setupClient: func() *fake.FakeDynamicClient {
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
getSampleKeptnMetric("my-metric", "3.0"),
)
return fakeClient
},
query: "keptnmetric/default/my-metric",
want: 3.0,
wantErr: false,
},
{
name: "KeptnMetric not found",
setupClient: func() *fake.FakeDynamicClient {
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
)
return fakeClient
},
query: "keptnmetric/default/my-metric",
want: 0,
wantErr: true,
},
{
name: "KeptnMetric with invalid value",
setupClient: func() *fake.FakeDynamicClient {
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
getSampleKeptnMetric("my-metric", "invalid"),
)
return fakeClient
},
query: "keptnmetric/default/my-metric",
want: 0,
wantErr: true,
},
{
name: "KeptnMetric with no value",
setupClient: func() *fake.FakeDynamicClient {
keptnMetric := getSampleKeptnMetric("my-metric", "")
data := keptnMetric.Object
delete(data, "status")
keptnMetric.SetUnstructuredContent(data)
fakeClient := fake.NewSimpleDynamicClient(
runtime.NewScheme(),
keptnMetric,
)
return fakeClient
},
query: "keptnmetric/default/my-metric",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := &KeptnProvider{
client: tt.setupClient(),
}
got, err := k.RunQuery(tt.query)
if tt.wantErr {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
require.Equalf(t, tt.want, got, "RunQuery(%v)", tt.query)
})
}
}
func TestKeptnProvider_RunQueryAnalysis(t *testing.T) {
tests := []struct {
name string
setupClient func() *fake.FakeDynamicClient
// verificationFunc() will run in a separate go routine
// and check if the expected resources are created
verificationFunc func(fakeClient *fake.FakeDynamicClient) error
query string
want float64
wantErr bool
}{
{
name: "get passed Analysis",
setupClient: func() *fake.FakeDynamicClient {
scheme := runtime.NewScheme()
scheme.AddKnownTypes(analysisResource.GroupVersion())
scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: analysisResource.Group, Version: analysisResource.Version, Kind: "AnalysisList"}, &unstructured.UnstructuredList{})
fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
analysisResource: "AnalysisList",
},
)
return fakeClient
},
verificationFunc: func(fakeClient *fake.FakeDynamicClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
return errors.New("timed out waiting for the condition")
case <-time.After(100 * time.Millisecond):
// verify the creation of the expected resource
list, err := fakeClient.Resource(analysisResource).
Namespace("default").
List(ctx, v1.ListOptions{
Limit: 1,
})
if err != nil || len(list.Items) == 0 {
continue
}
createdAnalysis := list.Items[0]
require.Equal(t, map[string]interface{}{
"analysisDefinition": map[string]interface{}{
"name": "my-analysis",
},
"args": map[string]interface{}{
"foo": "bar",
"bar": "foo",
},
"timeframe": map[string]interface{}{
"recent": "5m",
},
}, createdAnalysis.Object["spec"])
err = unstructured.SetNestedMap(
createdAnalysis.Object,
map[string]interface{}{
"state": "Completed",
"pass": true,
},
"status",
)
require.Nil(t, err)
_, err = fakeClient.Resource(analysisResource).Namespace("default").Update(ctx, &createdAnalysis, v1.UpdateOptions{})
require.Nil(t, err)
return nil
}
}
},
query: "analysis/default/my-analysis/5m/foo=bar;bar=foo",
want: 1,
wantErr: false,
},
{
name: "get failed Analysis",
setupClient: func() *fake.FakeDynamicClient {
scheme := runtime.NewScheme()
scheme.AddKnownTypes(analysisResource.GroupVersion())
scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: analysisResource.Group, Version: analysisResource.Version, Kind: "AnalysisList"}, &unstructured.UnstructuredList{})
fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
analysisResource: "AnalysisList",
},
)
return fakeClient
},
verificationFunc: func(fakeClient *fake.FakeDynamicClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
return errors.New("timed out waiting for the condition")
case <-time.After(10 * time.Millisecond):
// verify the creation of the expected resource
list, err := fakeClient.Resource(analysisResource).
Namespace("default").
List(ctx, v1.ListOptions{
Limit: 1,
})
if err != nil || len(list.Items) == 0 {
continue
}
createdAnalysis := list.Items[0]
require.Equal(t, map[string]interface{}{
"analysisDefinition": map[string]interface{}{
"name": "my-analysis",
},
"args": map[string]interface{}{},
"timeframe": map[string]interface{}{
"recent": "1m",
},
}, createdAnalysis.Object["spec"])
err = unstructured.SetNestedMap(
createdAnalysis.Object,
map[string]interface{}{
"state": "Completed",
"pass": false,
},
"status",
)
require.Nil(t, err)
_, err = fakeClient.Resource(analysisResource).Namespace("default").Update(ctx, &createdAnalysis, v1.UpdateOptions{})
require.Nil(t, err)
return nil
}
}
},
query: "analysis/default/my-analysis",
want: 0,
wantErr: false,
},
{
name: "analysis does not finish",
setupClient: func() *fake.FakeDynamicClient {
scheme := runtime.NewScheme()
scheme.AddKnownTypes(analysisResource.GroupVersion())
scheme.AddKnownTypeWithName(schema.GroupVersionKind{Group: analysisResource.Group, Version: analysisResource.Version, Kind: "AnalysisList"}, &unstructured.UnstructuredList{})
fakeClient := fake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
analysisResource: "AnalysisList",
},
)
return fakeClient
},
verificationFunc: func(fakeClient *fake.FakeDynamicClient) error {
return nil
},
query: "analysis/default/my-analysis",
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := tt.setupClient()
k := &KeptnProvider{
client: fakeClient,
analysisTimeout: 1 * time.Second,
}
ctx := context.Background()
grp, ctx := errgroup.WithContext(ctx)
grp.Go(func() error {
return tt.verificationFunc(fakeClient)
})
got, err := k.RunQuery(tt.query)
if tt.wantErr {
require.NotNil(t, err)
} else {
require.Nil(t, err)
}
err = grp.Wait()
require.Nil(t, err)
require.Equalf(t, tt.want, got, "RunQuery(%v)", tt.query)
// verify that all created Analysis resources have been cleaned up
list, err := fakeClient.Resource(analysisResource).
Namespace("default").
List(ctx, v1.ListOptions{
Limit: 1,
})
require.NoError(t, err)
require.Empty(t, list.Items)
})
}
}
func getSampleKeptnMetric(metricName, value string) *unstructured.Unstructured {
keptnMetric := &unstructured.Unstructured{}
keptnMetric.SetUnstructuredContent(map[string]interface{}{
"apiVersion": fmt.Sprintf("metrics.keptn.sh/%s", apiVersion),
"kind": "KeptnMetric",
"metadata": map[string]interface{}{
"name": metricName,
"namespace": "default",
},
"spec": map[string]interface{}{
"fetchIntervalSeconds": "2",
"provider": map[string]interface{}{
"name": "my-provider",
},
"query": "my-query",
},
"status": map[string]interface{}{
"value": value,
},
})
return keptnMetric
}