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

fix: Move GetSLIs() and GetShipyard() to keptn.ConfigClient #806

Merged
merged 18 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion internal/event_handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func getEventHandler(ctx context.Context, event cloudevents.Event, clientFactory

switch aType := keptnEvent.(type) {
case *monitoring.ConfigureMonitoringAdapter:
return monitoring.NewConfigureMonitoringEventHandler(keptnEvent.(*monitoring.ConfigureMonitoringAdapter), dtClient, kClient, keptn.NewConfigClient(clientFactory.CreateResourceClient()), clientFactory.CreateServiceClient(), keptn.NewDefaultCredentialsChecker()), nil
return monitoring.NewConfigureMonitoringEventHandler(keptnEvent.(*monitoring.ConfigureMonitoringAdapter), dtClient, kClient, keptn.NewConfigClient(clientFactory.CreateResourceClient()), keptn.NewConfigClient(clientFactory.CreateResourceClient()), clientFactory.CreateServiceClient(), keptn.NewDefaultCredentialsChecker()), nil
case *problem.ProblemAdapter:
return problem.NewProblemEventHandler(keptnEvent.(*problem.ProblemAdapter), kClient), nil
case *action.ActionTriggeredAdapter:
Expand Down
125 changes: 121 additions & 4 deletions internal/keptn/config_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import (
"fmt"

keptn "github.com/keptn/go-utils/pkg/lib"
keptnapi "github.com/keptn/go-utils/pkg/lib/keptn"
keptnv2 "github.com/keptn/go-utils/pkg/lib/v0_2_0"
"gopkg.in/yaml.v2"

"github.com/keptn-contrib/dynatrace-service/internal/dynatrace"
)

// SLOReaderInterface provides functionality for getting SLOs.
type SLOReaderInterface interface {
// SLIAndSLOReaderInterface provides functionality for getting SLIs and SLOs.
type SLIAndSLOReaderInterface interface {
// GetSLIs gets the SLIs stored for the specified project, stage and service.
// First, the configuration of project-level is retrieved, which is then overridden by configuration on stage level, and then overridden by configuration on service level.
GetSLIs(project string, stage string, service string) (map[string]string, error)

// GetSLOs gets the SLOs stored for exactly the specified project, stage and service.
GetSLOs(project string, stage string, service string) (*keptn.ServiceLevelObjectives, error)
}
Expand All @@ -25,18 +31,25 @@ type SLIAndSLOWriterInterface interface {
UploadSLOs(project string, stage string, service string, slos *keptn.ServiceLevelObjectives) error
}

// SLOAndSLIClientInterface provides functionality for getting SLOs and uploading SLIs and SLOs.
// SLOAndSLIClientInterface provides functionality for getting and uploading SLIs and SLOs.
type SLOAndSLIClientInterface interface {
SLOReaderInterface
SLIAndSLOReaderInterface
SLIAndSLOWriterInterface
}

// ShipyardReaderInterface provides functionality for getting a project's shipyard.
type ShipyardReaderInterface interface {
// GetShipyard returns the shipyard definition of a project.
GetShipyard(project string) (*keptnv2.Shipyard, error)
}

// DynatraceConfigReaderInterface provides functionality for getting a Dynatrace config.
type DynatraceConfigReaderInterface interface {
// GetDynatraceConfig gets the Dynatrace config for the specified project, stage and service, checking first on the service, then stage and then project level.
GetDynatraceConfig(project string, stage string, service string) (string, error)
}

const shipyardFilename = "shipyard.yaml"
const sloFilename = "slo.yaml"
const sliFilename = "dynatrace/sli.yaml"
const configFilename = "dynatrace/dynatrace.conf.yaml"
Expand Down Expand Up @@ -93,3 +106,107 @@ func (rc *ConfigClient) UploadSLIs(project string, stage string, service string,
func (rc *ConfigClient) GetDynatraceConfig(project string, stage string, service string) (string, error) {
return rc.client.GetResource(project, stage, service, configFilename)
}

// GetShipyard returns the shipyard definition of a project.
func (rc *ConfigClient) GetShipyard(project string) (*keptnv2.Shipyard, error) {
shipyard, err := rc.getShipyard(project)
if err != nil {
return nil, fmt.Errorf("failed to retrieve shipyard for project %s: %w", project, err)
}
return shipyard, nil
}

func (rc *ConfigClient) getShipyard(project string) (*keptnv2.Shipyard, error) {
shipyardResource, err := rc.client.GetProjectResource(project, shipyardFilename)
if err != nil {
return nil, err
}

shipyard := keptnv2.Shipyard{}
err = yaml.Unmarshal([]byte(shipyardResource), &shipyard)
if err != nil {
return nil, err
}

return &shipyard, nil
}

type sliMap map[string]string

func (m *sliMap) insertOrUpdateMany(x map[string]string) {
for key, value := range x {
map[string]string(*m)[key] = value
}
}

// GetSLIs gets the SLIs stored for the specified project, stage and service.
// First, the configuration of project-level is retrieved, which is then overridden by configuration on stage level, and then overridden by configuration on service level.
func (rc *ConfigClient) GetSLIs(project string, stage string, service string) (map[string]string, error) {
slis := make(sliMap)

// try to get SLI config from project
if project != "" {
projectSLIs, err := getSLIsFromResource(func() (string, error) { return rc.client.GetProjectResource(project, sliFilename) })
if err != nil {
return nil, err
}

slis.insertOrUpdateMany(projectSLIs)
}

// try to get SLI config from stage
if project != "" && stage != "" {
stageSLIs, err := getSLIsFromResource(func() (string, error) { return rc.client.GetStageResource(project, stage, sliFilename) })
if err != nil {
return nil, err
}

slis.insertOrUpdateMany(stageSLIs)
}

// try to get SLI config from service
if project != "" && stage != "" && service != "" {
serviceSLIs, err := getSLIsFromResource(func() (string, error) { return rc.client.GetServiceResource(project, stage, service, sliFilename) })
if err != nil {
return nil, err
}

slis.insertOrUpdateMany(serviceSLIs)
}

return slis, nil
}

type resourceGetterFunc func() (string, error)

// getSLIsFromResource uses the specified function to get a resource and returns the SLIs as a map.
// If is is not possible to get the resource for any other reason than it is not found, or it is not possible to unmarshal the file or it doesn't contain any indicators, an error is returned.
func getSLIsFromResource(resourceGetter resourceGetterFunc) (map[string]string, error) {
resource, err := resourceGetter()
if err != nil {
var rnfErrorType *ResourceNotFoundError
if errors.As(err, &rnfErrorType) {
return nil, nil
}

return nil, err
}

return readSLIsFromResource(resource)
}

// readSLIsFromResource unmarshals a resource as a SLIConfig and returns the SLIs as a map.
// If it is not possible to unmarshal the file or it doesn't contain any indicators, an error is returned.
func readSLIsFromResource(resource string) (map[string]string, error) {
sliConfig := keptnapi.SLIConfig{}
err := yaml.Unmarshal([]byte(resource), &sliConfig)
if err != nil {
return nil, fmt.Errorf("failed to parse SLI YAML: %w", err)
}

if len(sliConfig.Indicators) == 0 {
return nil, errors.New("missing required field: indicators")
}

return sliConfig.Indicators, nil
}
190 changes: 190 additions & 0 deletions internal/keptn/config_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package keptn

import (
"io/ioutil"
"testing"

"github.com/stretchr/testify/assert"
)

const testProject = "my-project"
const testStage = "my-stage"
const testService = "my-service"

// TestConfigClient_GetSLIsNoneDefined tests that getting SLIs when none have been defined returns an empty map but no error.
func TestConfigClient_GetSLIsNoneDefined(t *testing.T) {
rc := NewConfigClient(&mockResourceClient{t: t})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.Empty(t, slis)
assert.NoError(t, err)
}

// TestConfigClient_GetSLIsWithOverrides tests that service-level SLIs override stage or project-level SLIs and stage-level SLIs override project-level ones.
// In addition any SLIs defined only at a project or stage level should also be returned.
func TestConfigClient_GetSLIsWithOverrides(t *testing.T) {
rc := NewConfigClient(
&mockResourceClient{
t: t,
projectResource: getProjectResource(t),
stageResource: getStageResource(t),
serviceResource: getServiceResource(t)})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.NoError(t, err)

expectedSLIs := map[string]string{
"sli_a": "metricSelector=builtin:service.response.time:splitBy():percentile(95)&entitySelector=tag(keptn_project:my-project),tag(keptn_stage:my-stage),tag(kept_service:my-service)",
"sli_b": "metricSelector=builtin:service.response.time:splitBy():percentile(90)&entitySelector=tag(keptn_project:my-project),tag(keptn_stage:my-stage),tag(kept_service:my-service)",
"sli_c": "metricSelector=builtin:service.response.time:splitBy():percentile(80)&entitySelector=tag(keptn_project:my-project),tag(keptn_stage:my-stage),tag(kept_service:my-service)",
"sli_d": "metricSelector=builtin:service.response.time:splitBy():percentile(75)&entitySelector=tag(keptn_project:my-project),tag(keptn_stage:my-stage),tag(kept_service:my-service)",
"sli_e": "metricSelector=builtin:service.response.time:splitBy():percentile(70)&entitySelector=tag(keptn_project:my-project),tag(keptn_stage:my-stage)",
"sli_f": "metricSelector=builtin:service.response.time:splitBy():percentile(55)&entitySelector=tag(keptn_project:my-project)",
}

if !assert.EqualValues(t, len(expectedSLIs), len(slis)) {
return
}

for expectedKey, expectedValue := range expectedSLIs {
value, ok := slis[expectedKey]
assert.True(t, ok)
assert.EqualValues(t, expectedValue, value)
}
}

// TestConfigClient_GetSLIsInvalidYAMLCausesError tests that an invalid SLI YAML resource produces an error.
func TestConfigClient_GetSLIsInvalidYAMLCausesError(t *testing.T) {
rc := NewConfigClient(
&mockResourceClient{
t: t,
projectResource: getInvalidYAMLResource(t)})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.Nil(t, slis)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse SLI YAML")
}

// TestConfigClient_GetSLIsRetrievalErrorCausesError tests that resource retrieval errors produce an error.
func TestConfigClient_GetSLIsRetrievalErrorCausesError(t *testing.T) {
rc := NewConfigClient(
&mockResourceClient{
t: t,
serviceResource: &mockResource{err: &ResourceRetrievalFailedError{ResourceError{uri: testSLIResourceURI, project: testProject, stage: testStage, service: testService}, "Connection error"}}})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.Nil(t, slis)
assert.Error(t, err)
var rrfErrorType *ResourceRetrievalFailedError
assert.ErrorAs(t, err, &rrfErrorType)
}

// TestConfigClient_GetSLIsEmptySLIFileCausesError tests that an empty SLI file produces an error.
func TestConfigClient_GetSLIsEmptySLIFileCausesError(t *testing.T) {
rc := NewConfigClient(
&mockResourceClient{
t: t,
serviceResource: &mockResource{err: &ResourceEmptyError{uri: testSLIResourceURI, project: testProject, stage: testStage, service: testService}}})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.Nil(t, slis)
assert.Error(t, err)
var rrfErrorType *ResourceEmptyError
assert.ErrorAs(t, err, &rrfErrorType)
}

// TestConfigClient_GetSLIsNoIndicatorsCausesError tests that an SLI file containing no indicators produces an error.
func TestConfigClient_GetSLIsNoIndicatorsCausesError(t *testing.T) {
rc := NewConfigClient(
&mockResourceClient{
t: t,
serviceResource: getNoIndicatorsResource(t)})
slis, err := rc.GetSLIs(testProject, testStage, testService)
assert.Nil(t, slis)
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing required field")
}

const testDataFolder = "./testdata/config_client/get_slis"

func getNoIndicatorsResource(t *testing.T) *mockResource {
return &mockResource{resource: loadResource(t, testDataFolder+"/sli_no_indicators.yaml")}
}

func getInvalidYAMLResource(t *testing.T) *mockResource {
return &mockResource{resource: loadResource(t, testDataFolder+"/sli.invalid_yaml")}
}

func getServiceResource(t *testing.T) *mockResource {
return &mockResource{resource: loadResource(t, testDataFolder+"/sli_service.yaml")}
}

func getStageResource(t *testing.T) *mockResource {
return &mockResource{resource: loadResource(t, testDataFolder+"/sli_stage.yaml")}
}

func getProjectResource(t *testing.T) *mockResource {
return &mockResource{resource: loadResource(t, testDataFolder+"/sli_project.yaml")}
}

func loadResource(t *testing.T, filename string) string {
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
return string(content)
}

type mockResource struct {
resource string
err error
}

const testSLIResourceURI = "dynatrace/sli.yaml"

type mockResourceClient struct {
t *testing.T
projectResource *mockResource
stageResource *mockResource
serviceResource *mockResource
}

func (rc *mockResourceClient) GetResource(project string, stage string, service string, resourceURI string) (string, error) {
rc.t.Fatalf("GetResource() should not be needed in this mock!")
return "", nil
}

func (rc *mockResourceClient) GetProjectResource(project string, resourceURI string) (string, error) {
assert.EqualValues(rc.t, testProject, project)
assert.EqualValues(rc.t, testSLIResourceURI, resourceURI)

if rc.projectResource == nil {
return "", &ResourceNotFoundError{project: project, uri: resourceURI}
}

return rc.projectResource.resource, rc.projectResource.err
}

func (rc *mockResourceClient) GetStageResource(project string, stage string, resourceURI string) (string, error) {
assert.EqualValues(rc.t, testProject, project)
assert.EqualValues(rc.t, testStage, stage)
assert.EqualValues(rc.t, testSLIResourceURI, resourceURI)

if rc.stageResource == nil {
return "", &ResourceNotFoundError{project: project, stage: stage, uri: resourceURI}
}

return rc.stageResource.resource, rc.stageResource.err
}

func (rc *mockResourceClient) GetServiceResource(project string, stage string, service string, resourceURI string) (string, error) {
assert.EqualValues(rc.t, testProject, project)
assert.EqualValues(rc.t, testStage, stage)
assert.EqualValues(rc.t, testService, service)
assert.EqualValues(rc.t, testSLIResourceURI, resourceURI)

if rc.serviceResource == nil {
return "", &ResourceNotFoundError{project: project, stage: stage, service: service, uri: resourceURI}
}

return rc.serviceResource.resource, rc.serviceResource.err
}

func (rc *mockResourceClient) UploadResource(contentToUpload []byte, remoteResourceURI string, project string, stage string, service string) error {
rc.t.Fatalf("UploadResource() should not be needed in this mock!")
return nil
}
Loading