From 2134686803a50992701ced5c36d0d2cb6399102d Mon Sep 17 00:00:00 2001 From: Moshe Immerman Date: Sun, 22 Oct 2023 21:56:48 +0300 Subject: [PATCH] chore: switch to cel for alertmanager / aws checks --- checks/alertmanager.go | 8 +- checks/aws_config_rule.go | 100 ++++++++++++++------ checks/cloudwatch.go | 6 +- checks/common.go | 16 ++++ fixtures/aws/aws_config_rule_pass.yaml | 20 +++- fixtures/aws/cloudwatch_pass.yaml | 33 ++++--- fixtures/datasources/alertmanager_fail.yaml | 26 +++-- go.mod | 2 +- go.sum | 10 +- 9 files changed, 146 insertions(+), 75 deletions(-) diff --git a/checks/alertmanager.go b/checks/alertmanager.go index 8b08694d9..a1c7aec13 100644 --- a/checks/alertmanager.go +++ b/checks/alertmanager.go @@ -71,7 +71,11 @@ func (c *AlertManagerChecker) Check(ctx *context.Context, extConfig external.Che return results } - var alertMessages []map[string]any + type Alerts struct { + Alerts []map[string]interface{} `json:"alerts,omitempty"` + } + + var alertMessages []map[string]interface{} for _, alert := range alerts.Payload { alertMap := map[string]any{ "name": generateFullName(alert.Labels["alertname"], alert.Labels), @@ -83,7 +87,7 @@ func (c *AlertManagerChecker) Check(ctx *context.Context, extConfig external.Che alertMessages = append(alertMessages, alertMap) } - result.AddDetails(alertMessages) + result.AddDetails(Alerts{Alerts: alertMessages}) return results } diff --git a/checks/aws_config_rule.go b/checks/aws_config_rule.go index b57a454ed..cab405ad9 100644 --- a/checks/aws_config_rule.go +++ b/checks/aws_config_rule.go @@ -3,8 +3,8 @@ package checks import ( - "fmt" "strings" + "time" "github.com/aws/aws-sdk-go-v2/service/configservice" "github.com/aws/aws-sdk-go-v2/service/configservice/types" @@ -65,44 +65,82 @@ func (c *AwsConfigRuleChecker) Check(ctx *context.Context, extConfig external.Ch if err != nil { return results.Failf("failed to describe compliance rules: %v", err) } - var complianceResults pkg.Results + + type ConfigRuleResource struct { + ID string `json:"id"` + Annotation string `json:"annotation"` + Type string `json:"type"` + Recorded time.Time `json:"recorded"` + Mode string `json:"mode"` + } + + type ComplianceResult struct { + + // Supplementary information about how the evaluation determined the compliance. + Annotation string `json:"annotation"` + + ConfigRule string `json:"rule"` + + Description string `json:"description"` + // Indicates whether the Amazon Web Services resource complies with the Config + // rule that evaluated it. For the EvaluationResult data type, Config supports + // only the COMPLIANT , NON_COMPLIANT , and NOT_APPLICABLE values. Config does not + // support the INSUFFICIENT_DATA value for the EvaluationResult data type. + ComplianceType string `json:"type"` + + Resources []ConfigRuleResource `json:"resources"` + } + + var complianceResults []ComplianceResult + var failures []string for _, complianceRule := range output.ComplianceByConfigRules { if configRuleInRules(check.IgnoreRules, *complianceRule.ConfigRuleName) || complianceRule.Compliance.ComplianceType == "INSUFFICIENT_DATA" || complianceRule.Compliance.ComplianceType == "NOT_APPLICABLE" { continue } - if complianceRule.Compliance != nil { - var complianceResult *pkg.CheckResult - complianceCheck := check - complianceCheck.Description.Description = fmt.Sprintf("%s - checking compliance for config rule: %s", check.Description.Description, *complianceRule.ConfigRuleName) - if complianceRule.Compliance.ComplianceType != "COMPLIANT" { - complianceResult = pkg.Fail(complianceCheck, ctx.Canary) - complianceDetailsOutput, err := client.GetComplianceDetailsByConfigRule(ctx, &configservice.GetComplianceDetailsByConfigRuleInput{ - ComplianceTypes: []types.ComplianceType{ - "NON_COMPLIANT", - }, - ConfigRuleName: complianceRule.ConfigRuleName, + + if complianceRule.Compliance == nil { + continue + } + var data = ComplianceResult{ + ConfigRule: *complianceRule.ConfigRuleName, + ComplianceType: string(complianceRule.Compliance.ComplianceType), + } + + if complianceRule.Compliance.ComplianceType != "COMPLIANT" { + failures = append(failures, *complianceRule.ConfigRuleName) + complianceDetailsOutput, err := client.GetComplianceDetailsByConfigRule(ctx, &configservice.GetComplianceDetailsByConfigRuleInput{ + ComplianceTypes: []types.ComplianceType{ + "NON_COMPLIANT", + }, + ConfigRuleName: complianceRule.ConfigRuleName, + }) + if err != nil { + result.Failf("failed to get compliance details: %v", err) + continue + } + for _, result := range complianceDetailsOutput.EvaluationResults { + id := *result.EvaluationResultIdentifier.EvaluationResultQualifier + data.Resources = append(data.Resources, ConfigRuleResource{ + ID: *id.ResourceId, + Type: *id.ResourceType, + Mode: string(id.EvaluationMode), + Recorded: *result.ResultRecordedTime, + Annotation: *result.Annotation, }) - if err != nil { - complianceResult.Failf("failed to get compliance details: %v", err) - continue - } - var resources []string - for _, result := range complianceDetailsOutput.EvaluationResults { - if result.EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId != nil { - resources = append(resources, *result.EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId) - } - } - complianceResult.AddDetails(resources) - complianceResult.ResultMessage(strings.Join(resources, ",")) - } else { - complianceResult = pkg.Success(complianceCheck, ctx.Canary) - complianceResult.AddDetails(complianceRule) - complianceResult.ResultMessage(fmt.Sprintf("%s rule is %v", *complianceRule.ConfigRuleName, complianceRule.Compliance.ComplianceType)) } - complianceResults = append(complianceResults, complianceResult) } + complianceResults = append(complianceResults, data) } - return complianceResults + + if check.Test.IsEmpty() && len(failures) > 0 { + result.Failf(strings.Join(failures, ", ")) + } + if r, err := unstructure(map[string]interface{}{"rules": complianceResults}); err != nil { + result.Failf(err.Error()) + } else { + result.AddDetails(r) + } + return results } func configRuleInRules(rules []string, ruleName string) bool { diff --git a/checks/cloudwatch.go b/checks/cloudwatch.go index aeb745aae..ecca776ff 100644 --- a/checks/cloudwatch.go +++ b/checks/cloudwatch.go @@ -58,7 +58,11 @@ func (c *CloudWatchChecker) Check(ctx *context.Context, extConfig external.Check if err != nil { return results.ErrorMessage(err) } - result.AddDetails(alarms) + if o, err := unstructure(alarms); err != nil { + return results.ErrorMessage(err) + } else { + result.AddDetails(o) + } firing := []string{} for _, alarm := range alarms.MetricAlarms { if alarm.StateValue == types.StateValueAlarm { diff --git a/checks/common.go b/checks/common.go index 3d6cc782f..1464b1f20 100644 --- a/checks/common.go +++ b/checks/common.go @@ -51,6 +51,17 @@ func def(a, b string) string { return b } +// unstructure marshalls a struct to and from JSON to remove any type details +func unstructure(o any) (out interface{}, err error) { + + data, err := json.Marshal(o) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &out) + return out, err +} + func template(ctx *context.Context, template v1.Template) (string, error) { return gomplate.RunTemplate(ctx.Environment, template.Gomplate()) } @@ -107,6 +118,7 @@ func transform(ctx *context.Context, in *pkg.CheckResult) ([]*pkg.CheckResult, e // We use this label to set the transformed column to true // This label is used and then removed in pkg.FromV1 function r.Canary.Labels["transformed"] = "true" //nolint:goconst + r.Labels = t.Labels r.Transformed = true results = append(results, &r) } @@ -144,6 +156,9 @@ func transform(ctx *context.Context, in *pkg.CheckResult) ([]*pkg.CheckResult, e if t.DisplayType != "" { in.DisplayType = t.DisplayType } + if len(t.Labels) > 0 { + in.Labels = t.Labels + } if len(t.Data) > 0 { for k, v := range t.Data { in.Data[k] = v @@ -166,6 +181,7 @@ func GetJunitReportFromResults(canaryName string, results []*pkg.CheckResult) Ju test.Name = result.Check.GetDescription() test.Message = result.Message test.Duration = float64(result.Duration) / 1000 + test.Properties = result.Labels testSuite.Duration += float64(result.Duration) / 1000 if result.Pass { testSuite.Passed++ diff --git a/fixtures/aws/aws_config_rule_pass.yaml b/fixtures/aws/aws_config_rule_pass.yaml index ad48c0015..bc762a835 100644 --- a/fixtures/aws/aws_config_rule_pass.yaml +++ b/fixtures/aws/aws_config_rule_pass.yaml @@ -5,7 +5,19 @@ metadata: spec: interval: 30 awsConfigRule: - - description: Test config rules - name: non compliant rules - complianceTypes: - - NON_COMPLIANT \ No newline at end of file + - name: AWS Config Rule + region: "eu-west-1" + complianceTypes: [NON_COMPLIANT] + transform: + expr: | + results.rules.map(i, + i.resources.map(r, + { + 'name': i.rule + "/" + r.type + "/" + r.id, + 'description': i.rule, + 'icon': 'aws-config-alarm', + 'duration': time.Since(timestamp(r.recorded)).getMilliseconds(), + 'labels': {'id': r.id, 'type': r.type}, + 'message': i.description + i.annotation + r.annotation + }) + ).flatten().toJSON() diff --git a/fixtures/aws/cloudwatch_pass.yaml b/fixtures/aws/cloudwatch_pass.yaml index b748310ac..597c9f930 100644 --- a/fixtures/aws/cloudwatch_pass.yaml +++ b/fixtures/aws/cloudwatch_pass.yaml @@ -1,13 +1,20 @@ -cloudwatch: - - accessKey: - valueFrom: - secretKeyRef: - key: aws - name: access-key - secretKey: - valueFrom: - secretKeyRef: - key: aws - name: secrey-key - region: "us-east-1" - #skipTLSVerify: true +apiVersion: canaries.flanksource.com/v1 +kind: Canary +metadata: + name: cloudwatch +spec: + interval: 30 + cloudwatch: + region: "eu-west-1" + transform: + expr: | + results.MetricAlarms.filter(i, i.StateValue != 'OK' ).map(i, + { + 'name': i.MetricName, + 'icon': 'aws-cloudwatch-alarm', + 'duration': time.Since(timestamp(i.StateTransitionedTimestamp)).getMilliseconds(), + 'labels': fromAWSMap(i.Dimensions). + merge({'arn': i.AlarmArn}), + 'message': "%s (%s) %s".format([i.AlarmDescription, i.AlarmArn, fromAWSMap(i.Dimensions)]), + } + ).toJSON() diff --git a/fixtures/datasources/alertmanager_fail.yaml b/fixtures/datasources/alertmanager_fail.yaml index 07f4c9ee0..45f8cbcee 100644 --- a/fixtures/datasources/alertmanager_fail.yaml +++ b/fixtures/datasources/alertmanager_fail.yaml @@ -15,18 +15,14 @@ spec: - KubeScheduler.* exclude_filters: namespace: elastic-system - transform: - javascript: | - var out = _.map(results, function(r) { - return { - name: r.name, - labels: r.labels, - icon: 'alert', - message: r.message, - description: r.message, - status: 'unhealthy', - } - }) - JSON.stringify(out); - test: - template: "true" + transform: + expr: | + results.alerts.map(r, + { + 'name': r.name + r.fingerprint, + 'labels': r.labels, + 'icon': 'alert', + 'message': r.message, + 'description': r.message, + } + ).toJSON() diff --git a/go.mod b/go.mod index 5abc126f6..b7be68d8d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/fergusstrange/embedded-postgres v1.24.0 github.com/flanksource/commons v1.17.0 github.com/flanksource/duty v1.0.201 - github.com/flanksource/gomplate/v3 v3.20.18 + github.com/flanksource/gomplate/v3 v3.20.19 github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 github.com/flanksource/kommons v0.31.4 github.com/flanksource/postq v1.0.0 diff --git a/go.sum b/go.sum index 8240c2962..a99236571 100644 --- a/go.sum +++ b/go.sum @@ -818,8 +818,8 @@ github.com/flanksource/commons v1.17.0/go.mod h1:RDdQI0/QYC4GzicbDaXIvBPjWuQWKLz github.com/flanksource/duty v1.0.201 h1:c8r02bfuF47E2svK+qXCLHKaSqOCZZHKPj+v54eimqc= github.com/flanksource/duty v1.0.201/go.mod h1:aO1uXnT1eVtiIcicriK4brqJLmeXgbrYWtNR0H5IkLE= github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9oZXuxfNFGd/iGSdc= -github.com/flanksource/gomplate/v3 v3.20.18 h1:qYiznMxhq+Zau5iWnVzW1yDzA1deHOsmo6yldCN7JhQ= -github.com/flanksource/gomplate/v3 v3.20.18/go.mod h1:2GgHZ2vWmtDspJMBfUIryOuzJSwc8jU7Kw9fDLr0TMA= +github.com/flanksource/gomplate/v3 v3.20.19 h1:xl+XMYWXtlrO6FfU+VxwjNwX4/oBK3/soOtHRvUt2us= +github.com/flanksource/gomplate/v3 v3.20.19/go.mod h1:2GgHZ2vWmtDspJMBfUIryOuzJSwc8jU7Kw9fDLr0TMA= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc/go.mod h1:4pQhmF+TnVqJroQKY8wSnSp+T18oLson6YQ2M0qPHfQ= github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 h1:s6jf6P1pRfdvksVFjIXFRfnimvEYUR0/Mmla1EIjiRM= github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7/go.mod h1:BH5gh9JyEAuuWVP6Q5y9h43VozS0RfKyjNpM9L4v4hw= @@ -2318,22 +2318,16 @@ k8s.io/api v0.24.2/go.mod h1:AHqbSkTm6YrQ0ObxjO3Pmp/ubFF/KuM7jU+3khoBsOg= k8s.io/api v0.26.4/go.mod h1:WwKEXU3R1rgCZ77AYa7DFksd9/BAIKyOmRlbVxgvjCk= k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= -k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= -k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apimachinery v0.26.4/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= -k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= -k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= k8s.io/cli-runtime v0.28.0 h1:Tcz1nnccXZDNIzoH6EwjCs+7ezkUGhorzCweEvlVOFg= k8s.io/cli-runtime v0.28.0/go.mod h1:U+ySmOKBm/JUCmebhmecXeTwNN1RzI7DW4+OM8Oryas= k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= -k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= -k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= k8s.io/component-base v0.28.1 h1:LA4AujMlK2mr0tZbQDZkjWbdhTV5bRyEyAFe0TJxlWg= k8s.io/component-base v0.28.1/go.mod h1:jI11OyhbX21Qtbav7JkhehyBsIRfnO8oEgoAR12ArIU= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=