diff --git a/pkg/api/utils/resourceUtils.go b/pkg/api/utils/resourceUtils.go index 66ece412..9bd3129e 100644 --- a/pkg/api/utils/resourceUtils.go +++ b/pkg/api/utils/resourceUtils.go @@ -69,8 +69,8 @@ func (r *ResourceHandler) getHTTPClient() *http.Client { return r.HTTPClient } -// CreateServiceResources creates a service resource -func (r *ResourceHandler) CreateServiceResources(project string, stage string, service string, resources []*models.Resource) (*models.EventContext, *models.Error) { +// CreateResources creates a resource for the specified entity +func (r *ResourceHandler) CreateResources(project string, stage string, service string, resources []*models.Resource) (*models.EventContext, *models.Error) { copiedResources := make([]*models.Resource, len(resources), len(resources)) for i, val := range resources { @@ -86,5 +86,11 @@ func (r *ResourceHandler) CreateServiceResources(project string, stage string, s return nil, buildErrorResponse(err.Error()) } - return post(r.Scheme+"://"+r.BaseURL+"/v1/project/"+project+"/stage/"+stage+"/service/"+service+"/resource", requestStr, r) + if project != "" && stage != "" && service != "" { + return post(r.Scheme+"://"+r.BaseURL+"/v1/project/"+project+"/stage/"+stage+"/service/"+service+"/resource", requestStr, r) + } else if project != "" && stage != "" && service == "" { + return post(r.Scheme+"://"+r.BaseURL+"/v1/project/"+project+"/stage/"+stage+"/resource", requestStr, r) + } else { + return post(r.Scheme+"://"+r.BaseURL+"/v1/project/"+project+"/resource", requestStr, r) + } } diff --git a/pkg/configuration-service/utils/sliUtils.go b/pkg/configuration-service/utils/sliUtils.go new file mode 100644 index 00000000..c785fd18 --- /dev/null +++ b/pkg/configuration-service/utils/sliUtils.go @@ -0,0 +1,84 @@ +package utils + +import ( + "strings" + + "github.com/keptn/go-utils/pkg/configuration-service/models" + "gopkg.in/yaml.v2" +) + +// SLIConfig represents the struct of a SLI file +type SLIConfig struct { + Indicators map[string]string `json:"indicators" yaml:"indicators"` +} + +// GetSLIConfiguration retrieves the SLI configuration for a service considering SLI configuration on stage and project level. +// First, the configuration of project-level is retrieved, which is then overridden by configuration on stage level, +// overridden by configuration on service level. +func (r *ResourceHandler) GetSLIConfiguration(project string, stage string, service string, resourceURI string) (map[string]string, error) { + var res *models.Resource + var err error + SLIs := make(map[string]string) + + // get sli config from project + if project != "" { + res, err = r.GetProjectResource(project, resourceURI) + if err != nil { + // return error except "resource not found" type + if !strings.Contains(err.Error(), "resource not found") { + return nil, err + } + } + SLIs, err = addResourceContentToSLIMap(SLIs, res) + if err != nil { + return nil, err + } + } + + // get sli config from stage + if project != "" && stage != "" { + res, err = r.GetStageResource(project, stage, resourceURI) + if err != nil { + // return error except "resource not found" type + if !strings.Contains(err.Error(), "resource not found") { + return nil, err + } + } + SLIs, err = addResourceContentToSLIMap(SLIs, res) + if err != nil { + return nil, err + } + } + + // get sli config from service + if project != "" && stage != "" && service != "" { + res, err = r.GetServiceResource(project, stage, service, resourceURI) + if err != nil { + // return error except "resource not found" type + if !strings.Contains(err.Error(), "resource not found") { + return nil, err + } + } + SLIs, err = addResourceContentToSLIMap(SLIs, res) + if err != nil { + return nil, err + } + } + + return SLIs, nil +} + +func addResourceContentToSLIMap(SLIs map[string]string, resource *models.Resource) (map[string]string, error) { + if resource != nil { + sliConfig := SLIConfig{} + err := yaml.Unmarshal([]byte(resource.ResourceContent), &sliConfig) + if err != nil { + return nil, err + } + + for key, value := range sliConfig.Indicators { + SLIs[key] = value + } + } + return SLIs, nil +} diff --git a/pkg/configuration-service/utils/sliUtils_test.go b/pkg/configuration-service/utils/sliUtils_test.go new file mode 100644 index 00000000..8e44fa9f --- /dev/null +++ b/pkg/configuration-service/utils/sliUtils_test.go @@ -0,0 +1,60 @@ +package utils + +import ( + "testing" + + "github.com/keptn/go-utils/pkg/configuration-service/models" +) + +// TestAddResourceContentToSLIMap +func TestAddResourceContentToSLIMap(t *testing.T) { + SLIs := make(map[string]string) + resource := &models.Resource{} + resourceURI := "provider/sli.yaml" + resource.ResourceURI = &resourceURI + resource.ResourceContent = `--- +indicators: + error_rate: "builtin:service.errors.total.count:merge(0):avg?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + response_time_p50: "builtin:service.response.time:merge(0):percentile(50)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + response_time_p90: "builtin:service.response.time:merge(0):percentile(90)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + response_time_p95: "builtin:service.response.time:merge(0):percentile(95)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + throughput: "builtin:service.requestCount.total:merge(0):count?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" +` + SLIs, _ = addResourceContentToSLIMap(SLIs, resource) + + if len(SLIs) != 5 { + t.Errorf("Unexpected lenght of SLI map") + } +} + +// TestAddResourceContentToSLIMap +func TestAddMultipleResourceContentToSLIMap(t *testing.T) { + SLIs := make(map[string]string) + resource := &models.Resource{} + resourceURI := "provider/sli.yaml" + resource.ResourceURI = &resourceURI + resource.ResourceContent = `--- +indicators: + error_rate: "not defined" + response_time_p50: "builtin:service.response.time:merge(0):percentile(50)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + response_time_p90: "builtin:service.response.time:merge(0):percentile(90)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + response_time_p95: "builtin:service.response.time:merge(0):percentile(95)?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + throughput: "builtin:service.requestCount.total:merge(0):count?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" +` + SLIs, _ = addResourceContentToSLIMap(SLIs, resource) + + resource.ResourceContent = `--- +indicators: + error_rate: "builtin:service.errors.total.count:merge(0):avg?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" + failure_rate: "builtin:service.requestCount.total:merge(0):count?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" +` + SLIs, _ = addResourceContentToSLIMap(SLIs, resource) + + if len(SLIs) != 6 { + t.Errorf("Unexpected length of SLI map") + } + + if SLIs["error_rate"] != "builtin:service.errors.total.count:merge(0):avg?scope=tag(keptn_project:$PROJECT),tag(keptn_stage:$STAGE),tag(keptn_service:$SERVICE),tag(keptn_deployment:$DEPLOYMENT)" { + t.Errorf("Unexpected value of error_rate SLI") + } +} diff --git a/pkg/events/keptnEvents.go b/pkg/events/keptnEvents.go index abadb694..48fbd600 100644 --- a/pkg/events/keptnEvents.go +++ b/pkg/events/keptnEvents.go @@ -93,9 +93,9 @@ type ConfigurationChangeEventData struct { // FileChangesGeneratedChart provides new content for the generated chart. // The key value pairs represent the URI within the chart (i.e. the key) and the new content (i.e. the value). FileChangesGeneratedChart map[string]string `json:"fileChangesGeneratedChart,omitempty"` - // FileChangesUmbrellaChart provides new content for the umbrealla chart. + // FileChangesUmbrellaChart provides new content for the umbrella chart. // The key value pairs represent the URI within the chart (i.e. the key) and the new content (i.e. the value). - FileChangesUmbrellaChart map[string]string `json:"fileChangesUmbreallaChart,omitempty"` + FileChangesUmbrellaChart map[string]string `json:"fileChangesUmbrellaChart,omitempty"` // Labels contains labels Labels map[string]string `json:"labels"` } @@ -146,6 +146,8 @@ type TestsFinishedEventData struct { End string `json:"end"` // Labels contains labels Labels map[string]string `json:"labels"` + // Result shows the status of the test + Result string `json:"result"` } // StartEvaluationEventData represents the data for a test finished event @@ -234,7 +236,7 @@ type ProblemEventData struct { // PID is a unique system identifier of the reported problem. PID string `json:"PID"` // ImpcatedEntity is an identifier of the impacted entity - ImpactedEntity string `json:"ImpactedEntities,omitempty"` + ImpactedEntity string `json:"ImpactedEntity,omitempty"` // Tags is a comma separated list of tags that are defined for all impacted entities. Tags string `json:"Tags,omitempty"` // Project is the name of the project @@ -275,7 +277,7 @@ type InternalGetSLIEventData struct { TestStrategy string `json:"teststrategy"` // DeploymentStrategy is the deployment strategy DeploymentStrategy string `json:"deploymentstrategy"` - Deployment string `json:"deployment"` + Deployment string `json:"deployment"` Indicators []string `json:"indicators"` CustomFilters []*SLIFilter `json:"customFilters"` // Labels contains labels @@ -297,7 +299,7 @@ type InternalGetSLIDoneEventData struct { IndicatorValues []*SLIResult `json:"indicatorValues"` // DeploymentStrategy is the deployment strategy DeploymentStrategy string `json:"deploymentstrategy"` - Deployment string `json:"deployment"` + Deployment string `json:"deployment"` // Labels contains labels Labels map[string]string `json:"labels"` } diff --git a/pkg/utils/helmUtils.go b/pkg/utils/helmUtils.go index 1c50e1ee..9dc7af43 100644 --- a/pkg/utils/helmUtils.go +++ b/pkg/utils/helmUtils.go @@ -98,7 +98,11 @@ func GetRenderedDeployments(ch *chart.Chart) ([]*appsv1.Deployment, error) { Time: timeconv.Now(), }, } - + ch.Values.Raw += ` +keptn: + project: prj, + service: svc, + deployment: dpl` renderedTemplates, err := renderutil.Render(ch, ch.Values, renderOpts) if err != nil { return nil, err @@ -139,7 +143,11 @@ func GetRenderedServices(ch *chart.Chart) ([]*corev1.Service, error) { Time: timeconv.Now(), }, } - + ch.Values.Raw += ` +keptn: + project: prj, + service: svc, + deployment: dpl` renderedTemplates, err := renderutil.Render(ch, ch.Values, renderOpts) if err != nil { return nil, err diff --git a/pkg/utils/keptnUtils.go b/pkg/utils/keptnUtils.go index c4e7db21..4cc6f957 100644 --- a/pkg/utils/keptnUtils.go +++ b/pkg/utils/keptnUtils.go @@ -1,9 +1,12 @@ package utils import ( + "log" + "regexp" + + "github.com/keptn/go-utils/pkg/configuration-service/utils" "github.com/keptn/go-utils/pkg/models" "gopkg.in/yaml.v2" - "github.com/keptn/go-utils/pkg/configuration-service/utils" ) // KeptnHandler provides an interface to keptn resources @@ -32,3 +35,17 @@ func (k *KeptnHandler) GetShipyard(project string) (*models.Shipyard, error) { } return &shipyard, nil } + +// ValidateKeptnEntityName checks whether the provided name represents a valid +// project, service, or stage name +func ValidateKeptnEntityName(name string) bool { + if len(name) == 0 { + return false + } + reg, err := regexp.Compile(`(^[a-z][a-z0-9-]*[a-z0-9]$)|(^[a-z][a-z0-9]*)`) + if err != nil { + log.Fatal(err) + } + processedString := reg.FindString(name) + return len(processedString) == len(name) +} diff --git a/pkg/utils/keptnUtils_test.go b/pkg/utils/keptnUtils_test.go new file mode 100644 index 00000000..85c9de6f --- /dev/null +++ b/pkg/utils/keptnUtils_test.go @@ -0,0 +1,56 @@ +package utils + +import ( + "math/rand" + "testing" + "time" +) + +// generateStringWithSpecialChars generates a string of the given length +// and containing at least one special character and digit. +func generateStringWithSpecialChars(length int) string { + rand.Seed(time.Now().UnixNano()) + + digits := "0123456789" + specials := "~=+%^*/()[]{}/!@#$?|" + all := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + digits + specials + + buf := make([]byte, length) + buf[0] = digits[rand.Intn(len(digits))] + buf[1] = specials[rand.Intn(len(specials))] + + for i := 2; i < length; i++ { + buf[i] = all[rand.Intn(len(all))] + } + + rand.Shuffle(len(buf), func(i, j int) { + buf[i], buf[j] = buf[j], buf[i] + }) + + str := string(buf) + + return str +} + +// TestInvalidKeptnEntityName tests whether a random string containing a special character or digit +// does not pass the name validation. +func TestInvalidKeptnEntityName(t *testing.T) { + invalidName := generateStringWithSpecialChars(8) + if ValidateKeptnEntityName(invalidName) { + t.Fatalf("%s starts with upper case letter(s) or contains special character(s), but passed the name validation", invalidName) + } +} + +func TestInvalidKeptnEntityName2(t *testing.T) { + if ValidateKeptnEntityName("sockshop-") { + t.Fatalf("project name must not end with hyphen") + } +} + +func TestValidKeptnEntityName(t *testing.T) { + if !ValidateKeptnEntityName("sockshop-test") { + t.Fatalf("project should be valid") + } +} diff --git a/pkg/utils/kubeUtils.go b/pkg/utils/kubeUtils.go index 0ff58d82..2e81d655 100644 --- a/pkg/utils/kubeUtils.go +++ b/pkg/utils/kubeUtils.go @@ -14,6 +14,8 @@ import ( typesv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + // Initialize all known client auth plugins. _ "github.com/Azure/go-autorest/autorest" _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -211,3 +213,31 @@ func GetKeptnDomain(useInClusterConfig bool) (string, error) { } return cm.Data["app_domain"], nil } + +// CreateNamespace creates a new Kubernetes namespace with the provided name +func CreateNamespace(useInClusterConfig bool, namespace string) error { + + ns := &typesv1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + clientset, err := GetClientset(useInClusterConfig) + if err != nil { + return err + } + _, err = clientset.CoreV1().Namespaces().Create(ns) + return err +} + +// ExistsNamespace checks whether a namespace with the provided name exists +func ExistsNamespace(useInClusterConfig bool, namespace string) (bool, error) { + clientset, err := GetClientset(useInClusterConfig) + if err != nil { + return false, err + } + _, err = clientset.CoreV1().Namespaces().Get(namespace, metav1.GetOptions{}) + if err != nil { + if statusErr, ok := err.(*apierr.StatusError); ok && statusErr.ErrStatus.Reason == metav1.StatusReasonNotFound { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/releasenotes/releasenotes_V0.6.0.md b/releasenotes/releasenotes_V0.6.0.md new file mode 100644 index 00000000..115eda7b --- /dev/null +++ b/releasenotes/releasenotes_V0.6.0.md @@ -0,0 +1,8 @@ +# Release Notes 0.6.0 + +## New Features +- Added result property to `TestsFinishedEventData` [#542](https://github.com/keptn/keptn/issues/542) +- Added method for validating Keptn entity name [#1261](https://github.com/keptn/keptn/issues/1261) +- Added utility to create namespaces [#1231](https://github.com/keptn/keptn/issues/1231) +- Added helper function to get SLI config for service considering stage and project configs [#1192](https://github.com/keptn/keptn/issues/1192) + diff --git a/version b/version index 8f0916f7..a918a2aa 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.5.0 +0.6.0