mirror of
https://github.com/fluxcd/flagger.git
synced 2026-04-15 06:57:34 +00:00
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>
382 lines
9.8 KiB
Go
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
|
|
}
|