diff --git a/.github/workflows/postgres-and-push-test.yml b/.github/workflows/e2e-operator.yml similarity index 93% rename from .github/workflows/postgres-and-push-test.yml rename to .github/workflows/e2e-operator.yml index 8047b3443..5910701c9 100644 --- a/.github/workflows/postgres-and-push-test.yml +++ b/.github/workflows/e2e-operator.yml @@ -17,7 +17,7 @@ on: - "**.yaml" - "**.yml" - "test/**" -name: Postgres-and-Push-Test +name: Operator E2E Test permissions: contents: read jobs: @@ -41,4 +41,4 @@ jobs: cache- - run: make bin - name: Test - run: ./test/e2e-postgres-push.sh + run: ./test/e2e-operator.sh diff --git a/api/v1/canary_types.go b/api/v1/canary_types.go index adca8d8d3..5d3bb3598 100644 --- a/api/v1/canary_types.go +++ b/api/v1/canary_types.go @@ -74,6 +74,7 @@ type CanarySpec struct { AlertManager []AlertManagerCheck `yaml:"alertmanager,omitempty" json:"alertmanager,omitempty"` Dynatrace []DynatraceCheck `yaml:"dynatrace,omitempty" json:"dynatrace,omitempty"` AzureDevops []AzureDevopsCheck `yaml:"azureDevops,omitempty" json:"azureDevops,omitempty"` + Webhook *WebhookCheck `yaml:"webhook,omitempty" json:"webhook,omitempty"` // interval (in seconds) to run checks on Deprecated in favor of Schedule Interval uint64 `yaml:"interval,omitempty" json:"interval,omitempty"` // Schedule to run checks on. Supports all cron expression, example: '30 3-6,20-23 * * *'. For more info about cron expression syntax see https://en.wikipedia.org/wiki/Cron diff --git a/api/v1/checks.go b/api/v1/checks.go index 2645b75b1..73eb43bea 100644 --- a/api/v1/checks.go +++ b/api/v1/checks.go @@ -1358,6 +1358,22 @@ func (c AzureDevopsCheck) GetEndpoint() string { return c.Project } +type WebhookCheck struct { + Description `yaml:",inline" json:",inline"` + Templatable `yaml:",inline" json:",inline"` + + // Token is an optional authorization token to run this check + Token *types.EnvVar `yaml:"token,omitempty" json:"token,omitempty"` +} + +func (c WebhookCheck) GetType() string { + return "webhook" +} + +func (c WebhookCheck) GetEndpoint() string { + return "" +} + var AllChecks = []external.Check{ AlertManagerCheck{}, AwsConfigCheck{}, @@ -1396,4 +1412,5 @@ var AllChecks = []external.Check{ ResticCheck{}, S3Check{}, TCPCheck{}, + WebhookCheck{}, } diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index cbfaf3448..b598aa2dd 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -621,6 +621,11 @@ func (in *CanarySpec) DeepCopyInto(out *CanarySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(WebhookCheck) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanarySpec. @@ -3140,3 +3145,25 @@ func (in *VarSource) DeepCopy() *VarSource { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebhookCheck) DeepCopyInto(out *WebhookCheck) { + *out = *in + in.Description.DeepCopyInto(&out.Description) + out.Templatable = in.Templatable + if in.Token != nil { + in, out := &in.Token, &out.Token + *out = new(types.EnvVar) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookCheck. +func (in *WebhookCheck) DeepCopy() *WebhookCheck { + if in == nil { + return nil + } + out := new(WebhookCheck) + in.DeepCopyInto(out) + return out +} diff --git a/checks/common.go b/checks/common.go index 075190316..50957d529 100644 --- a/checks/common.go +++ b/checks/common.go @@ -6,11 +6,13 @@ import ( "time" _ "github.com/robertkrimen/otto/underscore" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/flanksource/canary-checker/api/context" v1 "github.com/flanksource/canary-checker/api/v1" "github.com/flanksource/canary-checker/pkg" "github.com/flanksource/canary-checker/pkg/utils" + cUtils "github.com/flanksource/commons/utils" "github.com/flanksource/gomplate/v3" "github.com/robfig/cron/v3" ) @@ -44,13 +46,6 @@ func getNextRuntime(canary v1.Canary, lastRuntime time.Time) (*time.Time, error) return &t, nil } -func def(a, b string) string { - if a != "" { - return a - } - 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) @@ -69,6 +64,7 @@ func template(ctx *context.Context, template v1.Template) (string, error) { return gomplate.RunTemplate(ctx.Environment, tpl) } +// transform generates new checks from the transformation template of the parent check func transform(ctx *context.Context, in *pkg.CheckResult) ([]*pkg.CheckResult, error) { var tpl v1.Template switch v := in.Check.(type) { @@ -105,22 +101,29 @@ func transform(ctx *context.Context, in *pkg.CheckResult) ([]*pkg.CheckResult, e if t.Name != "" && t.Name != in.Check.GetName() { // new check result created with a new name for _, t := range transformed { - t.Icon = def(t.Icon, in.Check.GetIcon()) - t.Description = def(t.Description, in.Check.GetDescription()) - t.Name = def(t.Name, in.Check.GetName()) - t.Type = def(t.Type, in.Check.GetType()) - t.Endpoint = def(t.Endpoint, in.Check.GetEndpoint()) - t.TransformDeleteStrategy = def(t.TransformDeleteStrategy, in.Check.GetTransformDeleteStrategy()) + t.Icon = cUtils.Coalesce(t.Icon, in.Check.GetIcon()) + t.Description = cUtils.Coalesce(t.Description, in.Check.GetDescription()) + t.Name = cUtils.Coalesce(t.Name, in.Check.GetName()) + t.Type = cUtils.Coalesce(t.Type, in.Check.GetType()) + t.Endpoint = cUtils.Coalesce(t.Endpoint, in.Check.GetEndpoint()) + t.TransformDeleteStrategy = cUtils.Coalesce(t.TransformDeleteStrategy, in.Check.GetTransformDeleteStrategy()) + r := t.ToCheckResult() r.Canary = in.Canary - r.Canary.Namespace = def(t.Namespace, r.Canary.Namespace) + r.Canary.Namespace = cUtils.Coalesce(t.Namespace, r.Canary.Namespace) if r.Canary.Labels == nil { r.Canary.Labels = make(map[string]string) } // We use this label to set the transformed column to true - // This label is used and then removed in pkg.FromV1 function + // this label are used and then removed in pkg.FromV1 function r.Canary.Labels["transformed"] = "true" //nolint:goconst + if t.DeletedAt != nil && !t.DeletedAt.IsZero() { + r.Canary.DeletionTimestamp = &metav1.Time{ + Time: *t.DeletedAt, + } + } + r.Labels = t.Labels r.Transformed = true results = append(results, &r) diff --git a/checks/metrics.go b/checks/metrics.go index 7da23518c..4ad261c16 100644 --- a/checks/metrics.go +++ b/checks/metrics.go @@ -82,7 +82,7 @@ func getLabelString(labels map[string]string) string { return s } -func exportCheckMetrics(ctx *context.Context, results pkg.Results) { +func ExportCheckMetrics(ctx *context.Context, results pkg.Results) { if len(results) == 0 { return } diff --git a/checks/runchecks.go b/checks/runchecks.go index c315403c1..90d9c2f22 100644 --- a/checks/runchecks.go +++ b/checks/runchecks.go @@ -11,10 +11,10 @@ import ( "github.com/flanksource/canary-checker/pkg" "github.com/flanksource/canary-checker/pkg/db" "github.com/flanksource/commons/logger" - "github.com/patrickmn/go-cache" + gocache "github.com/patrickmn/go-cache" ) -var checksCache = cache.New(5*time.Minute, 5*time.Minute) +var checksCache = gocache.New(5*time.Minute, 5*time.Minute) var DisabledChecks []string @@ -92,16 +92,16 @@ func RunChecks(ctx *context.Context) ([]*pkg.CheckResult, error) { } result := c.Run(ctx) - transformedResults := transformResults(ctx, result) + transformedResults := TransformResults(ctx, result) results = append(results, transformedResults...) - exportCheckMetrics(ctx, transformedResults) + ExportCheckMetrics(ctx, transformedResults) } - return processResults(ctx, results), nil + return ProcessResults(ctx, results), nil } -func transformResults(ctx *context.Context, in []*pkg.CheckResult) (out []*pkg.CheckResult) { +func TransformResults(ctx *context.Context, in []*pkg.CheckResult) (out []*pkg.CheckResult) { for _, r := range in { checkCtx := ctx.WithCheckResult(r) transformed, err := transform(checkCtx, r) @@ -117,7 +117,7 @@ func transformResults(ctx *context.Context, in []*pkg.CheckResult) (out []*pkg.C return out } -func processResults(ctx *context.Context, results []*pkg.CheckResult) []*pkg.CheckResult { +func ProcessResults(ctx *context.Context, results []*pkg.CheckResult) []*pkg.CheckResult { if ctx.Canary.Spec.ResultMode == "" { return results } diff --git a/checks/webhook.go b/checks/webhook.go new file mode 100644 index 000000000..b0506402f --- /dev/null +++ b/checks/webhook.go @@ -0,0 +1,3 @@ +package checks + +const WebhookCheckType = "webhook" diff --git a/cmd/serve.go b/cmd/serve.go index a15e17ff8..daa25c99e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -117,6 +117,9 @@ func serve() { e.POST("/api/push", api.PushHandler) e.GET("/api/details", api.DetailsHandler) e.GET("/api/topology", api.Topology) + + e.POST("/webhook/:id", api.WebhookHandler) + e.GET("/metrics", echo.WrapHandler(promhttp.HandlerFor(prom.DefaultGatherer, promhttp.HandlerOpts{}))) e.GET("/health", func(c echo.Context) error { return c.String(http.StatusOK, "OK") diff --git a/config/deploy/crd.yaml b/config/deploy/crd.yaml index 8747608ed..f5fcf8de3 100644 --- a/config/deploy/crd.yaml +++ b/config/deploy/crd.yaml @@ -5439,6 +5439,115 @@ spec: - name type: object type: array + webhook: + properties: + description: + description: Description for the check + type: string + display: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + icon: + description: Icon for overwriting default icon on the dashboard + type: string + labels: + additionalProperties: + type: string + description: Labels for the check + type: object + metrics: + description: Metrics to expose from check results + items: + properties: + labels: + items: + properties: + name: + type: string + value: + type: string + valueExpr: + type: string + required: + - name + type: object + type: array + name: + type: string + type: + type: string + value: + type: string + type: object + type: array + name: + description: Name of the check + type: string + test: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + token: + description: Token is an optional authorization token to run this check + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + transform: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + transformDeleteStrategy: + description: Transformed checks have a delete strategy on deletion they can either be marked healthy, unhealthy or left as is + type: string + required: + - name + type: object type: object status: description: CanaryStatus defines the observed state of Canary diff --git a/config/deploy/manifests.yaml b/config/deploy/manifests.yaml index d4d174373..4e94a3f73 100644 --- a/config/deploy/manifests.yaml +++ b/config/deploy/manifests.yaml @@ -5439,6 +5439,115 @@ spec: - name type: object type: array + webhook: + properties: + description: + description: Description for the check + type: string + display: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + icon: + description: Icon for overwriting default icon on the dashboard + type: string + labels: + additionalProperties: + type: string + description: Labels for the check + type: object + metrics: + description: Metrics to expose from check results + items: + properties: + labels: + items: + properties: + name: + type: string + value: + type: string + valueExpr: + type: string + required: + - name + type: object + type: array + name: + type: string + type: + type: string + value: + type: string + type: object + type: array + name: + description: Name of the check + type: string + test: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + token: + description: Token is an optional authorization token to run this check + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + transform: + properties: + expr: + type: string + javascript: + type: string + jsonPath: + type: string + template: + type: string + type: object + transformDeleteStrategy: + description: Transformed checks have a delete strategy on deletion they can either be marked healthy, unhealthy or left as is + type: string + required: + - name + type: object type: object status: description: CanaryStatus defines the observed state of Canary diff --git a/config/schemas/canary.schema.json b/config/schemas/canary.schema.json index b40faea98..83c440dfa 100644 --- a/config/schemas/canary.schema.json +++ b/config/schemas/canary.schema.json @@ -633,6 +633,9 @@ }, "type": "array" }, + "webhook": { + "$ref": "#/$defs/WebhookCheck" + }, "interval": { "type": "integer" }, @@ -3116,6 +3119,48 @@ }, "additionalProperties": false, "type": "object" + }, + "WebhookCheck": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/$defs/Labels" + }, + "transformDeleteStrategy": { + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/Metrics" + }, + "type": "array" + }, + "test": { + "$ref": "#/$defs/Template" + }, + "display": { + "$ref": "#/$defs/Template" + }, + "transform": { + "$ref": "#/$defs/Template" + }, + "token": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] } } } \ No newline at end of file diff --git a/config/schemas/component.schema.json b/config/schemas/component.schema.json index be7245517..63dbc48a7 100644 --- a/config/schemas/component.schema.json +++ b/config/schemas/component.schema.json @@ -612,6 +612,9 @@ }, "type": "array" }, + "webhook": { + "$ref": "#/$defs/WebhookCheck" + }, "interval": { "type": "integer" }, @@ -3512,6 +3515,48 @@ }, "additionalProperties": false, "type": "object" + }, + "WebhookCheck": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/$defs/Labels" + }, + "transformDeleteStrategy": { + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/Metrics" + }, + "type": "array" + }, + "test": { + "$ref": "#/$defs/Template" + }, + "display": { + "$ref": "#/$defs/Template" + }, + "transform": { + "$ref": "#/$defs/Template" + }, + "token": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] } } } \ No newline at end of file diff --git a/config/schemas/health_webhook.schema.json b/config/schemas/health_webhook.schema.json new file mode 100644 index 000000000..a8ffc1f34 --- /dev/null +++ b/config/schemas/health_webhook.schema.json @@ -0,0 +1,174 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/flanksource/canary-checker/api/v1/webhook-check", + "$ref": "#/$defs/WebhookCheck", + "$defs": { + "ConfigMapKeySelector": { + "properties": { + "name": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + }, + "EnvVar": { + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "valueFrom": { + "$ref": "#/$defs/EnvVarSource" + } + }, + "additionalProperties": false, + "type": "object" + }, + "EnvVarSource": { + "properties": { + "configMapKeyRef": { + "$ref": "#/$defs/ConfigMapKeySelector" + }, + "secretKeyRef": { + "$ref": "#/$defs/SecretKeySelector" + } + }, + "additionalProperties": false, + "type": "object" + }, + "Labels": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + }, + "MetricLabel": { + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "valueExpr": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + }, + "MetricLabels": { + "items": { + "$ref": "#/$defs/MetricLabel" + }, + "type": "array" + }, + "Metrics": { + "properties": { + "name": { + "type": "string" + }, + "labels": { + "$ref": "#/$defs/MetricLabels" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "SecretKeySelector": { + "properties": { + "name": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "key" + ] + }, + "Template": { + "properties": { + "template": { + "type": "string" + }, + "jsonPath": { + "type": "string" + }, + "expr": { + "type": "string" + }, + "javascript": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "WebhookCheck": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/$defs/Labels" + }, + "transformDeleteStrategy": { + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/Metrics" + }, + "type": "array" + }, + "test": { + "$ref": "#/$defs/Template" + }, + "display": { + "$ref": "#/$defs/Template" + }, + "transform": { + "$ref": "#/$defs/Template" + }, + "token": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] + } + } +} \ No newline at end of file diff --git a/config/schemas/topology.schema.json b/config/schemas/topology.schema.json index 368a4e4ed..c1d9b6efb 100644 --- a/config/schemas/topology.schema.json +++ b/config/schemas/topology.schema.json @@ -612,6 +612,9 @@ }, "type": "array" }, + "webhook": { + "$ref": "#/$defs/WebhookCheck" + }, "interval": { "type": "integer" }, @@ -3563,6 +3566,48 @@ }, "additionalProperties": false, "type": "object" + }, + "WebhookCheck": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/$defs/Labels" + }, + "transformDeleteStrategy": { + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/Metrics" + }, + "type": "array" + }, + "test": { + "$ref": "#/$defs/Template" + }, + "display": { + "$ref": "#/$defs/Template" + }, + "transform": { + "$ref": "#/$defs/Template" + }, + "token": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name" + ] } } } \ No newline at end of file diff --git a/fixtures/external/alertmanager.yaml b/fixtures/external/alertmanager.yaml new file mode 100644 index 000000000..d3b988ffd --- /dev/null +++ b/fixtures/external/alertmanager.yaml @@ -0,0 +1,24 @@ +apiVersion: canaries.flanksource.com/v1 +kind: Canary +metadata: + name: alert-manager-webhook-check + labels: + "Expected-Fail": "true" +spec: + schedule: "@every 1m" + webhook: + name: my-webhook + token: + value: webhook-auth-token + transform: + expr: | + results.json.alerts.map(r, + { + 'name': r.name + r.fingerprint, + 'labels': r.labels, + 'icon': 'alert', + 'message': r.annotations.summary, + 'description': r.annotations.description, + 'deletedAt': has(r.endsAt) ? r.endsAt : null, + } + ).toJSON() diff --git a/go.mod b/go.mod index f4ec4e3eb..10bf7b5e2 100644 --- a/go.mod +++ b/go.mod @@ -64,11 +64,16 @@ require ( github.com/spf13/pflag v1.0.5 go.mongodb.org/mongo-driver v1.12.1 go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 + go.opentelemetry.io/otel/sdk v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 golang.org/x/crypto v0.14.0 golang.org/x/net v0.17.0 golang.org/x/sync v0.4.0 google.golang.org/api v0.147.0 google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b + google.golang.org/grpc v1.59.0 gopkg.in/flanksource/yaml.v3 v3.2.3 gorm.io/gorm v1.25.5 gorm.io/plugin/prometheus v0.0.0-20230504115745-1aec2356381b @@ -234,11 +239,7 @@ require ( github.com/yuin/gopher-lua v1.1.0 // indirect github.com/zclconf/go-cty v1.14.1 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/sdk v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.starlark.net v0.0.0-20230925163745-10651d5192ab // indirect go.uber.org/multierr v1.11.0 // indirect @@ -255,7 +256,6 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect - google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect diff --git a/go.sum b/go.sum index a7a4620d4..db99acf13 100644 --- a/go.sum +++ b/go.sum @@ -973,6 +973,7 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1110,7 +1111,6 @@ github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6 github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= diff --git a/hack/generate-schemas/go.mod b/hack/generate-schemas/go.mod index 613c0f3dc..b0093a682 100644 --- a/hack/generate-schemas/go.mod +++ b/hack/generate-schemas/go.mod @@ -13,7 +13,7 @@ require ( cloud.google.com/go/compute v1.23.1 // indirect cloud.google.com/go/iam v1.1.3 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/flanksource/gomplate/v3 v3.20.19 // indirect + github.com/flanksource/gomplate/v3 v3.20.22 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect diff --git a/hack/generate-schemas/go.sum b/hack/generate-schemas/go.sum index a8752fc80..aec222f59 100644 --- a/hack/generate-schemas/go.sum +++ b/hack/generate-schemas/go.sum @@ -706,8 +706,8 @@ github.com/flanksource/commons v1.17.1/go.mod h1:RDdQI0/QYC4GzicbDaXIvBPjWuQWKLz github.com/flanksource/duty v1.0.205 h1:sQq+J4TMx69NnoM4XxBcJZ8P5HM5GjY/7zcuv/IQGTo= github.com/flanksource/duty v1.0.205/go.mod h1:V3fgZdrBgN47lloIz7MedwD/tq4ycHI8zFOApzUpFv4= github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9oZXuxfNFGd/iGSdc= -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/gomplate/v3 v3.20.22 h1:DOytkLh1aND8KruydfCnu9K9oRKPeoJj2qgFqQkGrpE= +github.com/flanksource/gomplate/v3 v3.20.22/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= diff --git a/pkg/api.go b/pkg/api.go index fe81eab06..8700be390 100644 --- a/pkg/api.go +++ b/pkg/api.go @@ -229,7 +229,7 @@ func FromExternalCheck(canary Canary, check external.Check) Check { } } -func FromResult(result CheckResult) CheckStatus { +func CheckStatusFromResult(result CheckResult) CheckStatus { return CheckStatus{ Status: result.Pass, Invalid: result.Invalid, @@ -261,10 +261,16 @@ func FromV1(canary v1.Canary, check external.Check, statuses ...CheckStatus) Che Statuses: statuses, Type: check.GetType(), } + if _, exists := c.Labels["transformed"]; exists { c.Transformed = true delete(c.Labels, "transformed") } + + if canary.DeletionTimestamp != nil && !canary.DeletionTimestamp.Time.IsZero() { + c.DeletedAt = &canary.DeletionTimestamp.Time + } + return c } @@ -469,6 +475,7 @@ type TransformedCheckResult struct { Invalid *bool `json:"invalid,omitempty"` Detail interface{} `json:"detail,omitempty"` Data map[string]interface{} `json:"data,omitempty"` + DeletedAt *time.Time `json:"deletedAt,omitempty"` Duration *int64 `json:"duration,omitempty"` Description string `json:"description,omitempty"` DisplayType string `json:"displayType,omitempty"` diff --git a/pkg/api/errors.go b/pkg/api/errors.go new file mode 100644 index 000000000..96db3d10a --- /dev/null +++ b/pkg/api/errors.go @@ -0,0 +1,86 @@ +package api + +import ( + "errors" + "fmt" +) + +// Application error codes. +// +// These are meant to be generic and they map well to HTTP error codes. +const ( + ECONFLICT = "conflict" + EFORBIDDEN = "forbidden" + EINTERNAL = "internal" + EINVALID = "invalid" + ENOTFOUND = "not_found" + ENOTIMPLEMENTED = "not_implemented" + EUNAUTHORIZED = "unauthorized" +) + +// Error represents an application-specific error. +type Error struct { + // Machine-readable error code. + Code string + + // Human-readable error message. + Message string + + // DebugInfo contains low-level internal error details that should only be logged. + // End-users should never see this. + DebugInfo string +} + +// Error implements the error interface. Not used by the application otherwise. +func (e *Error) Error() string { + return fmt.Sprintf("error: code=%s message=%s", e.Code, e.Message) +} + +// WithDebugInfo wraps an application error with a debug message. +func (e *Error) WithDebugInfo(msg string, args ...any) *Error { + e.DebugInfo = fmt.Sprintf(msg, args...) + return e +} + +// ErrorCode unwraps an application error and returns its code. +// Non-application errors always return EINTERNAL. +func ErrorCode(err error) string { + var e *Error + if err == nil { + return "" + } else if errors.As(err, &e) { + return e.Code + } + return EINTERNAL +} + +// ErrorMessage unwraps an application error and returns its message. +// Non-application errors always return "Internal error". +func ErrorMessage(err error) string { + var e *Error + if err == nil { + return "" + } else if errors.As(err, &e) { + return e.Message + } + return "Internal error." +} + +// ErrorDebugInfo unwraps an application error and returns its debug message. +func ErrorDebugInfo(err error) string { + var e *Error + if err == nil { + return "" + } else if errors.As(err, &e) { + return e.DebugInfo + } + return "" +} + +// Errorf is a helper function to return an Error with a given code and formatted message. +func Errorf(code string, format string, args ...any) *Error { + return &Error{ + Code: code, + Message: fmt.Sprintf(format, args...), + } +} diff --git a/pkg/api/http.go b/pkg/api/http.go new file mode 100644 index 000000000..06af5c6ad --- /dev/null +++ b/pkg/api/http.go @@ -0,0 +1,49 @@ +package api + +import ( + "net/http" + + "github.com/flanksource/commons/logger" + "github.com/labstack/echo/v4" +) + +type HTTPError struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` +} + +type HTTPSuccess struct { + Message string `json:"message"` + Payload any `json:"payload,omitempty"` +} + +// WriteError writes the error to the HTTP response with appropriate status code +func WriteError(c echo.Context, err error) error { + code, message := ErrorCode(err), ErrorMessage(err) + + if debugInfo := ErrorDebugInfo(err); debugInfo != "" { + logger.WithValues("code", code, "error", message).Errorf(debugInfo) + } + + return c.JSON(ErrorStatusCode(code), &HTTPError{Error: message}) +} + +// ErrorStatusCode returns the associated HTTP status code for an application error code. +func ErrorStatusCode(code string) int { + // lookup of application error codes to HTTP status codes. + var codes = map[string]int{ + ECONFLICT: http.StatusConflict, + EINVALID: http.StatusBadRequest, + ENOTFOUND: http.StatusNotFound, + EFORBIDDEN: http.StatusForbidden, + ENOTIMPLEMENTED: http.StatusNotImplemented, + EUNAUTHORIZED: http.StatusUnauthorized, + EINTERNAL: http.StatusInternalServerError, + } + + if v, ok := codes[code]; ok { + return v + } + + return http.StatusInternalServerError +} diff --git a/pkg/api/suite_test.go b/pkg/api/suite_test.go new file mode 100644 index 000000000..a50d178d5 --- /dev/null +++ b/pkg/api/suite_test.go @@ -0,0 +1,90 @@ +package api_test + +import ( + gocontext "context" + "fmt" + "net/http" + "testing" + + embeddedPG "github.com/fergusstrange/embedded-postgres" + apiContext "github.com/flanksource/canary-checker/api/context" + "github.com/flanksource/canary-checker/pkg/api" + "github.com/flanksource/canary-checker/pkg/cache" + "github.com/flanksource/canary-checker/pkg/db" + "github.com/flanksource/commons/logger" + "github.com/flanksource/duty" + "github.com/flanksource/duty/context" + "github.com/flanksource/duty/testutils" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gorm.io/gorm" +) + +var ( + testEchoServer *echo.Echo + testEchoServerPort = 9232 + dbPort = 9999 + ctx context.Context + + testDB *gorm.DB + testPool *pgxpool.Pool + + postgresServer *embeddedPG.EmbeddedPostgres +) + +func TestAPI(t *testing.T) { + RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "API Tests") +} + +var _ = ginkgo.BeforeSuite(func() { + var err error + + config, dbString := testutils.GetEmbeddedPGConfig("test_canary_job", dbPort) + postgresServer = embeddedPG.NewDatabase(config) + if err = postgresServer.Start(); err != nil { + ginkgo.Fail(err.Error()) + } + logger.Infof("Started postgres on port: %d", dbPort) + + if testDB, testPool, err = duty.SetupDB(dbString, nil); err != nil { + ginkgo.Fail(err.Error()) + } + cache.PostgresCache = cache.NewPostgresCache(testPool) + + // Set this because some functions directly use db.Gorm + db.Gorm = testDB + db.Pool = testPool + + ctx = context.NewContext(gocontext.Background()).WithDB(testDB, testPool) + apiContext.DefaultContext = ctx + + testEchoServer = echo.New() + testEchoServer.POST("/webhook/:id", api.WebhookHandler) + listenAddr := fmt.Sprintf(":%d", testEchoServerPort) + + go func() { + defer ginkgo.GinkgoRecover() // Required by ginkgo, if an assertion is made in a goroutine. + if err := testEchoServer.Start(listenAddr); err != nil { + if err == http.ErrServerClosed { + logger.Infof("Server closed") + } else { + ginkgo.Fail(fmt.Sprintf("Failed to start test server: %v", err)) + } + } + }() +}) + +var _ = ginkgo.AfterSuite(func() { + logger.Infof("Stopping test echo server") + if err := testEchoServer.Shutdown(gocontext.Background()); err != nil { + ginkgo.Fail(err.Error()) + } + + logger.Infof("Stopping postgres") + if err := postgresServer.Stop(); err != nil { + ginkgo.Fail(err.Error()) + } +}) diff --git a/pkg/api/utils.go b/pkg/api/utils.go index 242cc276d..1579c1b9c 100644 --- a/pkg/api/utils.go +++ b/pkg/api/utils.go @@ -4,6 +4,7 @@ import ( "github.com/labstack/echo/v4" ) +// Deprecated: use HTTPError func errorResonse(c echo.Context, err error, code int) error { e := map[string]string{"error": err.Error()} return c.JSON(code, e) diff --git a/pkg/api/webhook.go b/pkg/api/webhook.go new file mode 100644 index 000000000..3d285d164 --- /dev/null +++ b/pkg/api/webhook.go @@ -0,0 +1,119 @@ +package api + +import ( + goctx "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/flanksource/canary-checker/api/context" + v1 "github.com/flanksource/canary-checker/api/v1" + "github.com/flanksource/canary-checker/checks" + "github.com/flanksource/canary-checker/pkg" + "github.com/flanksource/canary-checker/pkg/cache" + "github.com/flanksource/canary-checker/pkg/db" + "github.com/flanksource/duty" + "github.com/flanksource/duty/models" + "github.com/labstack/echo/v4" +) + +const webhookBodyLimit = 10 * 1024 // 10 MB + +type CheckData struct { + Headers map[string]string `json:"headers"` + JSON map[string]any `json:"json,omitempty"` + Content string `json:"content,omitempty"` +} + +func WebhookHandler(c echo.Context) error { + id := c.Param("id") + + authToken := c.QueryParam("token") + if authToken == "" { + authToken = c.Request().Header.Get("Webhook-Token") + } + + data := CheckData{ + Headers: make(map[string]string), + } + for k := range c.Request().Header { + data.Headers[k] = c.Request().Header.Get(k) + } + + if c.Request().Header.Get("Content-Type") == "application/json" { + if err := json.NewDecoder(c.Request().Body).Decode(&data.JSON); err != nil { + return WriteError(c, err) + } + } else { + b, err := io.ReadAll(io.LimitReader(c.Request().Body, webhookBodyLimit)) + if err != nil { + return WriteError(c, err) + } + + data.Content = string(b) + } + + if err := webhookHandler(c.Request().Context(), id, authToken, data); err != nil { + return WriteError(c, err) + } + + return c.JSON(http.StatusOK, &HTTPSuccess{Message: "ok"}) +} + +func webhookHandler(ctx goctx.Context, id, authToken string, data CheckData) error { + webhookChecks, err := db.FindChecks(context.DefaultContext.Wrap(ctx), id, checks.WebhookCheckType) + if err != nil { + return err + } + + var check models.Check + if len(webhookChecks) == 0 { + return Errorf(ENOTFOUND, "check (%s) not found", id) + } else if len(webhookChecks) > 1 { + return Errorf(EINVALID, "multiple checks with name: %s were found. Please use the check id or modify the check to have a unique name", id) + } else { + check = webhookChecks[0] + } + + var canary *v1.Canary + if c, err := db.FindCanaryByID(check.CanaryID.String()); err != nil { + return fmt.Errorf("failed to get canary: %w", err) + } else if c == nil { + return Errorf(ENOTFOUND, "canary was not found (id:%s): %v", check.CanaryID.String(), err) + } else if canary, err = c.ToV1(); err != nil { + return err + } + + webhook := canary.Spec.Webhook + if webhook == nil { + return Errorf(ENOTFOUND, "no webhook checks found") + } + + // Authorization + if webhook.Token != nil { + token, err := duty.GetEnvValueFromCache(context.DefaultContext.Kubernetes(), *webhook.Token, canary.Namespace) + if err != nil { + return err + } + + if token != "" && token != authToken { + return Errorf(EUNAUTHORIZED, "invalid webhook token") + } + } + + result := pkg.Success(webhook, *canary) + result.AddDetails(data) + + results := []*pkg.CheckResult{result} + + scrapeCtx := context.New(context.DefaultContext.Kommons(), context.DefaultContext.Kubernetes(), db.Gorm, db.Pool, *canary) + transformedResults := checks.TransformResults(scrapeCtx, results) + + checks.ExportCheckMetrics(scrapeCtx, transformedResults) + for _, result := range transformedResults { + _ = cache.PostgresCache.Add(pkg.FromV1(result.Canary, result.Check), pkg.CheckStatusFromResult(*result)) + } + + return nil +} diff --git a/pkg/api/webhook_test.go b/pkg/api/webhook_test.go new file mode 100644 index 000000000..d1dd1c8c6 --- /dev/null +++ b/pkg/api/webhook_test.go @@ -0,0 +1,194 @@ +package api_test + +import ( + "encoding/json" + "fmt" + netHTTP "net/http" + "time" + + v1 "github.com/flanksource/canary-checker/api/v1" + "github.com/flanksource/canary-checker/checks" + "github.com/flanksource/canary-checker/pkg/db" + canaryJobs "github.com/flanksource/canary-checker/pkg/jobs/canary" + "github.com/flanksource/commons/http" + "github.com/flanksource/duty/job" + "github.com/flanksource/duty/models" + "github.com/flanksource/duty/types" + "github.com/google/uuid" + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("Test Sync Canary Job", ginkgo.Ordered, func() { + canarySpec := v1.CanarySpec{ + Schedule: "@every 1s", + Webhook: &v1.WebhookCheck{ + Description: v1.Description{ + Name: "my-webhook", + }, + Templatable: v1.Templatable{ + Transform: v1.Template{ + Expression: ` + results.json.alerts.map(r, + { + 'name': r.name + r.fingerprint, + 'labels': r.labels, + 'icon': 'alert', + 'message': r.annotations.summary, + 'description': r.annotations.description, + 'deletedAt': has(r.endsAt) ? r.endsAt : null, + } + ).toJSON()`, + }, + }, + Token: &types.EnvVar{ + ValueStatic: "my-token", + }, + }, + } + + var canaryM *models.Canary + client := http.NewClient().BaseURL(fmt.Sprintf("http://localhost:%d", testEchoServerPort)).Header("Content-Type", "application/json") + + ginkgo.It("should save a canary spec", func() { + b, err := json.Marshal(canarySpec) + Expect(err).To(BeNil()) + + var spec types.JSON + err = json.Unmarshal(b, &spec) + Expect(err).To(BeNil()) + + canaryM = &models.Canary{ + ID: uuid.New(), + Spec: spec, + Name: "alert-manager-canary", + } + err = testDB.Create(canaryM).Error + Expect(err).To(BeNil()) + + response, err := db.GetAllCanariesForSync(ctx, "") + Expect(err).To(BeNil()) + Expect(len(response)).To(Equal(1)) + }) + + ginkgo.It("schedule the canary job", func() { + canaryJobs.CanaryScheduler.Start() + jobCtx := job.JobRuntime{ + Context: ctx, + } + + err := canaryJobs.SyncCanaryJobs(jobCtx) + Expect(err).To(BeNil()) + }) + + ginkgo.It("Should have created the webhook check", func() { + var count = 30 + for { + time.Sleep(time.Second) // Wait for SyncCanaryJob to create the check + count-- + + var checks []models.Check + err := ctx.DB().Where("name = ?", canarySpec.Webhook.Name).Find(&checks).Error + Expect(err).To(BeNil()) + + if len(checks) == 1 { + break + } + + if len(checks) != 1 && count <= 0 { + ginkgo.Fail("expected check to be created") + } + } + }) + + ginkgo.It("Should forbid when webhook is called without the auth token", func() { + resp, err := client.R(ctx).Post(fmt.Sprintf("/webhook/%s", canarySpec.Webhook.Name), nil) + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(netHTTP.StatusUnauthorized)) + }) + + ginkgo.It("Should allow when webhook is called with the auth token", func() { + body := `{ + "version": "4", + "status": "firing", + "alerts": [ + { + "status": "firing", + "name": "first", + "labels": { + "severity": "critical", + "alertName": "ServerDown", + "location": "DataCenterA" + }, + "annotations": { + "summary": "Server in DataCenterA is down", + "description": "This alert indicates that a server in DataCenterA is currently down." + }, + "startsAt": "2023-10-30T08:00:00Z", + "generatorURL": "http://example.com/generatorURL/serverdown", + "fingerprint": "a1b2c3d4e5f6" + }, + { + "status": "resolved", + "labels": { + "severity": "warning", + "alertName": "HighCPUUsage", + "location": "DataCenterB" + }, + "annotations": { + "summary": "High CPU Usage in DataCenterB", + "description": "This alert indicates that there was high CPU usage in DataCenterB, but it is now resolved." + }, + "startsAt": "2023-10-30T09:00:00Z", + "generatorURL": "http://example.com/generatorURL/highcpuusage", + "name": "second", + "fingerprint": "x1y2z3w4v5" + } + ] +}` + resp, err := client.R(ctx).Post(fmt.Sprintf("/webhook/%s?token=%s", canarySpec.Webhook.Name, canarySpec.Webhook.Token.ValueStatic), body) + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(netHTTP.StatusOK)) + }) + + ginkgo.It("Should have created 2 new checks from the webhook", func() { + var result []models.Check + err := testDB.Where("type = ?", checks.WebhookCheckType).Where("name != ?", canarySpec.Webhook.Name).Find(&result).Error + Expect(err).To(BeNil()) + Expect(len(result)).To(Equal(2)) + }) + + ginkgo.It("Should have deleted one resolved alert from", func() { + body := `{ + "version": "4", + "status": "firing", + "alerts": [ + { + "status": "firing", + "name": "first", + "labels": { + "severity": "critical", + "alertName": "ServerDown", + "location": "DataCenterA" + }, + "annotations": { + "summary": "Server in DataCenterA is down", + "description": "This alert indicates that a server in DataCenterA is currently down." + }, + "startsAt": "2023-10-30T08:00:00Z", + "generatorURL": "http://example.com/generatorURL/serverdown", + "fingerprint": "a1b2c3d4e5f6", + "endsAt": "2023-10-30T09:15:00Z" + } + ] + }` + resp, err := client.R(ctx).Post(fmt.Sprintf("/webhook/%s?token=%s", canarySpec.Webhook.Name, canarySpec.Webhook.Token.ValueStatic), body) + Expect(err).To(BeNil()) + Expect(resp.StatusCode).To(Equal(netHTTP.StatusOK)) + + var result models.Check + err = testDB.Where("name = 'firsta1b2c3d4e5f6'").Find(&result).Error + Expect(err).To(BeNil()) + Expect(result.DeletedAt).To(Not(BeNil())) + }) +}) diff --git a/pkg/config.go b/pkg/config.go index 2770c6951..1cc299b5c 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -135,7 +135,7 @@ func ParseConfig(configfile string, datafile string) ([]v1.Canary, error) { return nil, err } - if len(config.Spec.GetAllChecks()) == 0 { + if len(config.Spec.GetAllChecks()) == 0 && config.Spec.Webhook == nil { // try just the specs: spec := v1.CanarySpec{} diff --git a/pkg/db/canary.go b/pkg/db/canary.go index 167291ac2..e8ced179f 100644 --- a/pkg/db/canary.go +++ b/pkg/db/canary.go @@ -96,6 +96,10 @@ func PersistCheck(check pkg.Check, canaryID uuid.UUID) (uuid.UUID, error) { "deleted_at": nil, } + if check.DeletedAt != nil { + assignments["deleted_at"] = check.DeletedAt + } + if err := Gorm.Clauses( clause.OnConflict{ Columns: []clause.Column{{Name: "canary_id"}, {Name: "type"}, {Name: "name"}, {Name: "agent_id"}}, @@ -278,6 +282,23 @@ func FindCheck(canary pkg.Canary, name string) (*pkg.Check, error) { return &model, nil } +func FindChecks(ctx context.Context, idOrName, checkType string) ([]models.Check, error) { + query := ctx.DB(). + Where("agent_id = ?", uuid.Nil.String()). + Where("type = ?", checkType). + Where("deleted_at IS NULL") + + if _, err := uuid.Parse(idOrName); err != nil { + query = query.Where("name = ?", idOrName) + } else { + query = query.Where("id = ?", idOrName) + } + + var checks []models.Check + err := query.Find(&checks).Error + return checks, err +} + func FindDeletedChecksSince(ctx context.Context, since time.Time) ([]string, error) { var ids []string err := ctx.DB().Model(&models.Check{}).Where("deleted_at > ?", since).Pluck("id", &ids).Error diff --git a/pkg/jobs/canary/canary_jobs.go b/pkg/jobs/canary/canary_jobs.go index 0adb8f37c..9c842400d 100644 --- a/pkg/jobs/canary/canary_jobs.go +++ b/pkg/jobs/canary/canary_jobs.go @@ -113,6 +113,10 @@ func (j CanaryJob) Run(ctx dutyjob.JobRuntime) error { } span.End() + if canaryCtx.Canary.Spec.Webhook != nil { + results = append(results, pkg.Success(canaryCtx.Canary.Spec.Webhook, canaryCtx.Canary)) + } + // Get transformed checks before and after, and then delete the olds ones that are not in new set existingTransformedChecks, _ := db.GetTransformedCheckIDs(ctx.Context, canaryID) var transformedChecksCreated []string @@ -127,7 +131,7 @@ func (j CanaryJob) Run(ctx dutyjob.JobRuntime) error { if logPass && result.Pass || logFail && !result.Pass { logger.Infof(result.String()) } - transformedChecksAdded := cache.PostgresCache.Add(pkg.FromV1(result.Canary, result.Check), pkg.FromResult(*result)) + transformedChecksAdded := cache.PostgresCache.Add(pkg.FromV1(result.Canary, result.Check), pkg.CheckStatusFromResult(*result)) transformedChecksCreated = append(transformedChecksCreated, transformedChecksAdded...) for _, checkID := range transformedChecksAdded { checkIDDeleteStrategyMap[checkID] = result.Check.GetTransformDeleteStrategy() @@ -218,7 +222,7 @@ func updateCanaryStatusAndEvent(canary v1.Canary, results []*pkg.CheckResult) { } // TODO Why is this here ? - push.Queue(pkg.FromV1(canary, result.Check), pkg.FromResult(*result)) + push.Queue(pkg.FromV1(canary, result.Check), pkg.CheckStatusFromResult(*result)) // Update status message if len(messages) == 1 { diff --git a/test/e2e-postgres-push.sh b/test/e2e-operator.sh similarity index 100% rename from test/e2e-postgres-push.sh rename to test/e2e-operator.sh