├── chart ├── crds │ └── crd.yaml ├── templates │ ├── serviceaccount.yaml │ ├── service.yaml │ ├── postgres-service.yaml │ ├── configmap.yaml │ ├── grafanaDashboard.yaml │ ├── clusterrolebinding.yaml │ ├── servicemonitor.yaml │ ├── ingress.yaml │ ├── postgres-statefulset.yaml │ └── postgres-secret.yaml ├── .helmignore └── Chart.yaml ├── fixtures ├── datasources │ ├── _post_setup.sh │ ├── folder_fail.yaml │ ├── GCP │ │ ├── database_backup.yaml │ │ └── folder_pass.yaml │ ├── SFTP │ │ ├── sftp_fail_connection.yaml │ │ └── sftp_pass.yaml │ ├── redis_pass.yaml │ ├── prometheus.yaml │ ├── redis_fail.yaml │ ├── mongo_pass.yaml │ ├── mssql_fail.yaml │ ├── kustomization.yaml │ ├── mysql_pass.yaml │ ├── postgres_fail.yaml │ ├── mssql_pass.yaml │ ├── mongo_fail.yaml │ ├── postgres_pass.yaml │ ├── mysql_fail.yaml │ ├── posgres_stateful_pass.yaml │ ├── _karina.yaml │ ├── alertmanager_mix.yaml │ ├── folder_pass.yaml │ ├── s3_bucket_fail.yaml │ └── go.mod ├── git │ ├── _image │ ├── kustomization.yaml │ ├── exec_checkout_pass.yaml │ ├── git_check_pass.yaml │ ├── git_test_expression_pass.yaml │ ├── git_pull_push_pass.yaml │ ├── gitea.values │ └── _setup.sh ├── restic │ ├── _image │ ├── _karina.yaml │ ├── restic_fail.yaml │ ├── restic_without_integrity_pass.yaml │ ├── restic_with_integrity_pass.yaml │ └── _setup.sh ├── topology │ ├── kustomization.yaml │ ├── single-check.yaml │ ├── inline-check.yaml │ ├── kube-dns.yaml │ ├── selector.yaml │ ├── component-with-parent-lookup.yml │ ├── canary-selector.yaml │ └── kubernetes-cluster-group-by.yaml ├── namespace.yml ├── aws │ ├── kustomization.yaml │ ├── s3-protocol.yaml │ ├── minimal │ │ ├── aws_exec_pass.yaml │ │ └── _setup.sh │ ├── cloudwatch_pass.yaml │ ├── s3_bucket_pass.yaml │ ├── aws_config_rule_pass.yaml │ └── aws_config_pass.yaml ├── ldap │ ├── kustomization.yaml │ └── ldap_pass.yaml ├── opensearch │ ├── kustomization.yaml │ ├── _post_setup.sh │ ├── opensearch_pass.yaml │ └── opensearch_fail.yaml ├── elasticsearch │ ├── kustomization.yaml │ ├── elasticsearch_pass.yaml │ ├── elasticsearch_fail.yaml │ └── _post_setup.sh ├── _setup.yaml ├── minimal │ ├── kustomization.yaml │ ├── tcp.yaml │ ├── icmp_fail.yaml │ ├── exec_pass.yaml │ ├── http_no_auth_pass.yaml │ ├── http_timeout_fail.yaml │ ├── exec_fail.yaml │ ├── exec_env_pass.yaml │ ├── exec_connection_aws_fail.yaml │ ├── http_auth.yaml │ ├── http_fail_connection.yaml │ ├── http_fail.yaml │ ├── exec_artifact.yaml │ ├── http-check-labels.yaml │ ├── http_template.yaml │ ├── metrics.yaml │ ├── http_pass_results_mode_pass.yaml │ ├── metrics-transformed.yaml │ ├── namespaced_check_pass.yaml │ ├── metrics-multiple.yaml │ ├── http_pass_single.yaml │ ├── cel.yaml │ ├── dns_fail.yaml │ └── dns_pass.yaml ├── kustomization.yaml ├── k8s │ ├── _setup.sh │ ├── kubernetes_pass.yaml │ ├── http_auth_configmap.yaml │ ├── junit_pass.yaml │ ├── http_auth_secret.yaml │ ├── kustomization.yaml │ ├── certmanager.yaml │ ├── kubernetes-minimal_pass.yaml │ ├── pod_pass.yaml │ ├── pod_fail.yaml │ ├── _karina.yaml │ ├── slow │ │ └── namespace_pass.yaml │ ├── cronjob_monitor.yaml │ ├── kubernetes_resource_namespace_pass.yaml │ ├── cronjob_monitor_fail.yaml │ ├── _setup.yaml │ ├── junit_fail.yaml │ ├── junit_pass_metrics.yaml │ ├── kubernetes_bundle.yaml │ ├── kubernetes_resource_service_fail.yaml │ ├── kubernetes_resource_pod_exit_code_pass.yaml │ ├── kubernetes_resource_service_pass.yaml │ └── kubernetes_resource_ingress_pass.yaml ├── quarantine │ ├── icmp_pass.yaml │ ├── s3_fail.yaml │ └── smb_pass.yaml ├── external │ ├── catalog.yaml │ ├── alertmanager.yaml │ └── dynatrace.yaml └── azure │ └── devops.yaml ├── hack ├── generate-schemas │ ├── .gitignore │ └── main.go └── boilerplate.go.txt ├── checks ├── webhook.go ├── timer.go ├── folder_test.go ├── folder_s3_test.go ├── mysql.go ├── postgres.go ├── mssql.go ├── stubs_windows.go ├── catalog.go ├── checker.go ├── folder_sftp.go ├── azure_devops_test.go ├── mongodb.go ├── database_backup.go ├── tcp.go ├── redis.go ├── github.go ├── ldap.go ├── folder_gcs.go ├── aws_config.go └── folder_s3.go ├── .dockerignore ├── config ├── namespace.yaml ├── kustomization.yaml ├── base │ ├── kustomization.yaml │ └── ingress.yaml └── schemas │ └── health_tcp.schema.json ├── PROJECT ├── fixtures-crd └── k8s │ └── _setup.yaml ├── canary-checker.properties ├── Tiltfile ├── .editorconfig ├── test ├── Makefile ├── nested-canaries │ └── ec2-http.yaml ├── aggregate-test │ ├── Procfile │ └── config │ │ ├── server1.yaml │ │ ├── server2.yaml │ │ └── main.yaml └── karina.yaml ├── cmd ├── output │ ├── output.go │ ├── junit.go │ └── csv.go ├── sync.go └── offline.go ├── pkg ├── topology │ ├── jobs.go │ ├── utils.go │ ├── suite_test.go │ ├── component_config_test.go │ ├── query.go │ └── component_check_test.go ├── api │ ├── utils.go │ ├── details.go │ ├── topology.go │ ├── api_test.go │ ├── http.go │ └── suite_test.go ├── runner │ ├── shutdown.go │ └── runner.go ├── cache │ └── postgres_util.go ├── sync │ ├── topology.go │ └── sync.go ├── db │ └── postgrest.go ├── labels │ └── labels.go ├── jobs │ ├── jobs.go │ └── canary │ │ ├── suite_test.go │ │ └── canary_jobs_test.go ├── dns │ └── dns.go └── controllers │ └── suite_test.go ├── .gitignore ├── .pre-commit-config.yaml ├── api ├── external │ ├── api.go │ ├── metrics.go │ └── zz_generated.deepcopy.go └── v1 │ ├── groupversion_info.go │ ├── db_types.go │ └── conditions.go ├── .github ├── workflows │ ├── build.yml │ ├── gotest.yml │ ├── e2e-operator.yml │ ├── aws-exec.yml │ ├── helm-test.yml │ └── lint.yml └── dependabot.yml ├── .golangci.yml ├── main.go ├── SECURITY.md └── .releaserc /chart/crds/crd.yaml: -------------------------------------------------------------------------------- 1 | ../../config/deploy/crd.yaml -------------------------------------------------------------------------------- /fixtures/datasources/_post_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | -------------------------------------------------------------------------------- /fixtures/git/_image: -------------------------------------------------------------------------------- 1 | flanksource/canary-checker-full 2 | -------------------------------------------------------------------------------- /fixtures/restic/_image: -------------------------------------------------------------------------------- 1 | flanksource/canary-checker-full 2 | -------------------------------------------------------------------------------- /hack/generate-schemas/.gitignore: -------------------------------------------------------------------------------- 1 | go.mod 2 | go.sum 3 | -------------------------------------------------------------------------------- /fixtures/topology/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - k8s-system.yaml -------------------------------------------------------------------------------- /checks/webhook.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | const WebhookCheckType = "webhook" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .idea/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | chart/ 7 | -------------------------------------------------------------------------------- /fixtures/namespace.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: canaries 5 | -------------------------------------------------------------------------------- /fixtures/git/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - git_check_pass.yaml 3 | - git_test_expression_pass.yaml 4 | - git_pull_push_pass.yaml 5 | -------------------------------------------------------------------------------- /config/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: canary-checker 6 | name: canary-checker 7 | -------------------------------------------------------------------------------- /fixtures/aws/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - ec2_pass.yaml 5 | - s3_bucket_pass.yaml 6 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: flanksource.com 2 | repo: github.com/flanksource/canary-checker 3 | resources: 4 | - group: canaries 5 | kind: Canary 6 | version: v1 7 | version: "2" 8 | -------------------------------------------------------------------------------- /fixtures-crd/k8s/_setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: secrets 5 | stringData: 6 | DOCKER_USERNAME: test 7 | DOCKER_PASSWORD: password 8 | -------------------------------------------------------------------------------- /fixtures/ldap/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: ldap 4 | resources: 5 | - _setup.yaml 6 | - ldap_pass.yaml 7 | -------------------------------------------------------------------------------- /canary-checker.properties: -------------------------------------------------------------------------------- 1 | # check.disabled.http=true 2 | # check.disabled.dns=false 3 | # check.disabled.s3=false 4 | 5 | # check.disabled.tcp=false 6 | 7 | # topology.runNow=true 8 | -------------------------------------------------------------------------------- /fixtures/opensearch/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canaries 4 | resources: 5 | - _setup.yaml 6 | - opensearch_fail.yaml 7 | - opensearch_pass.yaml 8 | -------------------------------------------------------------------------------- /fixtures/elasticsearch/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canaries 4 | resources: 5 | - _setup.yaml 6 | - elasticsearch_fail.yaml 7 | - elasticsearch_pass.yaml 8 | -------------------------------------------------------------------------------- /fixtures/_setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scheduling.k8s.io/v1 2 | kind: PriorityClass 3 | metadata: 4 | name: canary-checker-priority 5 | value: -1 6 | globalDefault: false 7 | description: "This priority class should be used for canary pods only." 8 | -------------------------------------------------------------------------------- /fixtures/minimal/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - dns_fail.yaml 5 | - dns_pass.yaml 6 | - http_fail.yaml 7 | - http_pass_single.yaml 8 | - http_timeout_fail.yaml -------------------------------------------------------------------------------- /fixtures/datasources/folder_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-fail 5 | spec: 6 | interval: 30 7 | folder: 8 | - path: /etc/ 9 | name: min count fail 10 | minCount: 100000 11 | maxAge: 4m -------------------------------------------------------------------------------- /fixtures/minimal/tcp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: tcp-check 5 | spec: 6 | schedule: "*/1 * * * *" 7 | tcp: 8 | - name: "flanksource website" 9 | endpoint: www.flanksource.com:80 10 | thresholdMillis: 1200 11 | -------------------------------------------------------------------------------- /fixtures/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | buildMetadata: [originAnnotations] 4 | resources: 5 | - _setup.yaml 6 | - datasources 7 | - k8s 8 | - minimal 9 | - git 10 | - ldap 11 | - opensearch 12 | - elasticsearch 13 | -------------------------------------------------------------------------------- /fixtures/k8s/_setup.sh: -------------------------------------------------------------------------------- 1 | docker pull public.ecr.aws/docker/library/busybox:1.33.1 2 | docker tag public.ecr.aws/docker/library/busybox:1.33.1 ttl.sh/flanksource-busybox:1.33.1 3 | docker tag public.ecr.aws/docker/library/busybox:1.33.1 docker.io/flanksource/busybox:1.33.1 4 | 5 | kubectl apply -k ../ 6 | -------------------------------------------------------------------------------- /fixtures/minimal/icmp_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: icmp-fail 5 | spec: 6 | interval: 30 7 | icmp: 8 | - endpoint: https://github.com 9 | thresholdMillis: 1 10 | packetLossThreshold: 5 11 | packetCount: 2 12 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # Build: tell Tilt what images to build from which directories 2 | # docker_build( './', dockerfile=Dockerfile.dev) 3 | default_registry('ttl.sh') 4 | custom_build( 5 | 'docker.io/flanksource/canary-checker', 6 | 'make linux && docker build -t $EXPECTED_REF . -f Dockerfile', 7 | ['pkg'], 8 | ) 9 | -------------------------------------------------------------------------------- /fixtures/quarantine/icmp_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: icmp 5 | spec: 6 | interval: 30 7 | icmp: 8 | - endpoint: https://api.github.com 9 | thresholdMillis: 600 10 | packetLossThreshold: 10 11 | packetCount: 2 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{js,jsx,ts,tsx,json,yaml}] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /config/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canary-checker 4 | resources: 5 | - ./base 6 | - ./namespace.yaml 7 | - ./deploy/crd.yaml 8 | images: 9 | - name: controller 10 | newName: docker.io/flanksource/canary-checker 11 | newTag: latest 12 | -------------------------------------------------------------------------------- /config/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canary-checker 4 | resources: 5 | - ./manager.yaml 6 | - ./ingress.yaml 7 | - ./rbac.yaml 8 | images: 9 | - name: controller 10 | newName: docker.io/flanksource/canary-checker 11 | newTag: latest 12 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | NAME=canary-checker 2 | LD_FLAGS=-ldflags "-w -s -X \"main.version=$(VERSION_TAG)\"" 3 | 4 | ifeq ($(VERSION),) 5 | VERSION_TAG=$(shell git describe --abbrev=0 --tags || echo latest) 6 | else 7 | VERSION_TAG=$(VERSION) 8 | endif 9 | 10 | .PHONY: build 11 | build: 12 | ginkgo build -r ./ -c $(LD_FLAGS) 13 | 14 | -------------------------------------------------------------------------------- /chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ .Values.serviceAccount.name }} 5 | {{- with .Values.serviceAccount.annotations }} 6 | annotations: 7 | {{- toYaml . | nindent 4 }} 8 | {{- end }} 9 | labels: 10 | {{- include "canary-checker.labels" . | nindent 4 }} 11 | -------------------------------------------------------------------------------- /test/nested-canaries/ec2-http.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: nested-http 5 | namespace: default 6 | spec: 7 | interval: 0 8 | http: 9 | - endpoint: "http://{{.PublicIpAddress}}" 10 | thresholdMillis: 3000 11 | responseCodes: [200] 12 | responseContent: "" 13 | -------------------------------------------------------------------------------- /fixtures/datasources/GCP/database_backup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: database-backup-example 5 | spec: 6 | interval: 60 7 | databaseBackup: 8 | - name: backup 9 | maxAge: 6h 10 | GCP: 11 | project: google-project-name 12 | instance: cloudsql-instance-name 13 | -------------------------------------------------------------------------------- /fixtures/datasources/SFTP/sftp_fail_connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: sftp-pass 5 | spec: 6 | interval: 30 7 | folder: 8 | - path: /tmp/premier-league 9 | name: sample sftp check 10 | sftpConnection: 11 | connection: connection://sftp/emirates 12 | maxCount: 10 -------------------------------------------------------------------------------- /fixtures/datasources/redis_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: redis-succeed 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | redis: 9 | - addr: "redis.canaries.svc.cluster.local:6379" 10 | name: redis ping check 11 | db: 0 12 | description: "The redis pass test" 13 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-pass 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: exec-pass-check 9 | description: "exec dummy check" 10 | script: | 11 | echo "hello" 12 | test: 13 | expr: 'results.stdout == "hello"' 14 | -------------------------------------------------------------------------------- /fixtures/topology/single-check.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: single-check 5 | spec: 6 | type: Website 7 | icon: Application 8 | schedule: "@every 5m" 9 | components: 10 | - checks: 11 | - selector: 12 | labelSelector: "check=http-200" 13 | name: single-check 14 | -------------------------------------------------------------------------------- /cmd/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func HandleOutput(report, outputFile string) error { 9 | if outputFile != "" { 10 | err := os.WriteFile(outputFile, []byte(report), 0755) 11 | if err != nil { 12 | return err 13 | } 14 | } else { 15 | fmt.Println(report) 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /fixtures/external/catalog.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: node-catalogs 5 | spec: 6 | interval: 30 7 | catalog: 8 | - name: ingress-catalog-check 9 | selector: 10 | - types: 11 | - Kubernetes::IngressClass 12 | test: 13 | expr: "size(results) > 0" 14 | 15 | -------------------------------------------------------------------------------- /fixtures/minimal/http_no_auth_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-no-auth 5 | annotations: 6 | trace: "true" 7 | spec: 8 | http: 9 | - name: http-no-auth 10 | url: https://httpbin.demo.aws.flanksource.com/headers 11 | test: 12 | expr: "! ('Authorization' in json.headers.keys())" 13 | -------------------------------------------------------------------------------- /pkg/topology/jobs.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import "github.com/flanksource/duty/job" 4 | 5 | var Jobs = []*job.Job{ 6 | ComponentConfigRun, 7 | ComponentCheckRun, 8 | CleanupSoftDeletedComponents, 9 | CleanupCanaries, 10 | CleanupChecks, 11 | CleanupMetricsGauges, 12 | ComponentCostRun, 13 | ComponentRelationshipSync, 14 | ComponentStatusSummarySync, 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: prometheus 5 | spec: 6 | interval: 30 7 | prometheus: 8 | - url: https://prometheus.demo.aws.flanksource.com/ 9 | name: prometheus-check 10 | query: kubernetes_build_info{job!~"kube-dns|coredns"} 11 | display: 12 | expr: results[0].git_version 13 | -------------------------------------------------------------------------------- /fixtures/minimal/http_timeout_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-fail-timeout 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | http: 10 | - endpoint: https://httpbin.demo.aws.flanksource.com/delay/2 11 | name: http fail timeout 12 | thresholdMillis: 100 13 | responseCodes: [200] 14 | -------------------------------------------------------------------------------- /fixtures/restic/_karina.yaml: -------------------------------------------------------------------------------- 1 | configFrom: 2 | - file: ../../test/karina.yaml 3 | s3: 4 | endpoint: http://minio.minio.svc.cluster.local:9000 5 | access_key: minio 6 | secret_key: minio123 7 | region: us-east1 8 | usePathStyle: true 9 | skipTLSVerify: true 10 | minio: 11 | version: RELEASE.2020-09-02T18-19-50Z 12 | access_key: minio 13 | secret_key: minio123 14 | replicas: 1 15 | -------------------------------------------------------------------------------- /chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "canary-checker.name" . }} 5 | labels: 6 | {{- include "canary-checker.labels" . | nindent 4 }} 7 | spec: 8 | ports: 9 | - port: 8080 10 | targetPort: 8080 11 | protocol: TCP 12 | name: http 13 | selector: 14 | {{- include "canary-checker.selectorLabels" . | nindent 4 }} 15 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | exec: 10 | - name: exec-fail-check 11 | description: "exec dummy check" 12 | script: | 13 | echo "hi there" 14 | test: 15 | expr: 'results.stdout == "hello"' 16 | 17 | -------------------------------------------------------------------------------- /chart/templates/postgres-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.db.external.create true }} 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: postgres 6 | labels: 7 | {{- include "postgresql.labels" . | nindent 4 }} 8 | spec: 9 | selector: 10 | app: postgresql 11 | {{- include "postgresql.selectorLabels" . | nindent 4 }} 12 | ports: 13 | - port: 5432 14 | targetPort: 5432 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /fixtures/datasources/redis_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: redis-fail 5 | namespace: canaries 6 | labels: 7 | "Expected-Fail": "true" 8 | spec: 9 | interval: 30 10 | redis: 11 | - addr: "redis.default--namespace:32004" #wrong host for the failure 12 | name: redis host failure 13 | db: 0 14 | description: "The redis fail test" 15 | -------------------------------------------------------------------------------- /fixtures/datasources/SFTP/sftp_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: sftp-pass 5 | spec: 6 | interval: 30 7 | folder: 8 | - path: /tmp 9 | name: sample sftp check 10 | sftpConnection: 11 | host: 192.168.1.5 12 | username: 13 | value: 14 | password: 15 | value: 16 | maxCount: 10 17 | -------------------------------------------------------------------------------- /fixtures/datasources/mongo_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mongo 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | mongodb: 9 | - url: mongodb://$(username):$(password)@mongo.canaries.svc.cluster.local:27017/?authSource=admin 10 | name: mongo ping check 11 | username: 12 | value: mongoadmin 13 | password: 14 | value: secret 15 | -------------------------------------------------------------------------------- /chart/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "canary-checker.name" . }} 5 | labels: 6 | {{- include "canary-checker.labels" . | nindent 4 }} 7 | data: 8 | canary-checker.properties: | 9 | {{- range $k, $v := .Values.disableChecks }} 10 | check.disabled.{{ $k }}={{ $v }} 11 | {{- end }} 12 | {{- range $k, $v := .Values.properties }} 13 | {{ $k }}={{ $v }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /fixtures/git/exec_checkout_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-checkout 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: exec-checkout 9 | description: "exec with git" 10 | script: | 11 | cat go.mod | head -n 1 12 | checkout: 13 | url: github.com/flanksource/duty 14 | test: 15 | expr: 'results.stdout == "module github.com/flanksource/duty"' 16 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /fixtures/datasources/mssql_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mssql-fail 5 | namespace: canaries 6 | labels: 7 | "Expected-Fail": "true" 8 | spec: 9 | interval: 30 10 | mssql: 11 | - url: "server=mssql.platformsystem;user id=sa;password=S0m3p@sswd;port=32010;database=master" #wrong server name for failure 12 | name: mssql servername 13 | query: "SELECT 1" 14 | results: 1 15 | -------------------------------------------------------------------------------- /fixtures/datasources/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canaries 4 | resources: 5 | - _setup.yaml 6 | - mongo_fail.yaml 7 | - mongo_pass.yaml 8 | - mssql_fail.yaml 9 | - mssql_pass.yaml 10 | - mysql_fail.yaml 11 | - mysql_pass.yaml 12 | - postgres_fail.yaml 13 | - postgres_pass.yaml 14 | - prometheus.yaml 15 | - redis_fail.yaml 16 | - redis_pass.yaml 17 | - alertmanager_mix.yaml 18 | -------------------------------------------------------------------------------- /fixtures/quarantine/s3_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-fail 5 | spec: 6 | interval: 30 7 | s3: 8 | - bucket: 9 | name: "test-bucket" 10 | region: "us-east-1" 11 | endpoint: "https://test-bucket.s3.us-east-1.amazonaws.com" 12 | secretKey: "****************" 13 | accessKey: "~~~~~~~~~~~~~~~~" 14 | objectPath: "path/to/object" 15 | skipTLSVerify: true 16 | -------------------------------------------------------------------------------- /fixtures/datasources/mysql_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mysql-pass 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | mysql: 9 | - url: "$(username):$(password)@tcp(mysql.canaries.svc.cluster.local:3306)/mysqldb" 10 | name: mysql ping check 11 | username: 12 | value: mysqladmin 13 | password: 14 | value: admin123 15 | query: "SELECT 1" 16 | results: 1 17 | -------------------------------------------------------------------------------- /fixtures/restic/restic_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: restic-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | restic: 9 | - repository: s3:http://minio.minio:9000/restic-canary-checker 10 | name: restic fail test 11 | password: 12 | value: S0m3p@sswd 13 | maxAge: 10s 14 | accessKey: 15 | value: minio 16 | secretLKey: 17 | value: minio123 18 | -------------------------------------------------------------------------------- /fixtures/git/git_check_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: github-pass 5 | spec: 6 | interval: 30 7 | github: 8 | - query: "SELECT * FROM commits('https://github.com/flanksource/commons')" 9 | name: github-check 10 | test: 11 | expr: size(results) > 0 12 | githubToken: 13 | valueFrom: 14 | secretKeyRef: 15 | name: github-token 16 | key: GITHUB_TOKEN 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bin/ 2 | .creds/ 3 | .release/ 4 | bin/ 5 | .idea/ 6 | .env 7 | .certs 8 | .kube 9 | docs/cli/ 10 | .DS_Store 11 | cover.out 12 | test.test 13 | test.out 14 | *.tgz 15 | *.log 16 | wait4x 17 | .vscode/ 18 | chart/templates/crd.yaml 19 | postgres-db/ 20 | ui/scripts/ 21 | Chart.lock 22 | chart/charts/ 23 | .downloads 24 | tmp/ 25 | ginkgo.report 26 | __debug* 27 | test-results.xml 28 | coverprofile.out 29 | junit-report.xml 30 | canary-checker.properties 31 | *.code-workspace 32 | -------------------------------------------------------------------------------- /fixtures/datasources/postgres_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | 8 | spec: 9 | interval: 30 10 | postgres: 11 | - url: "user=$(username) dbname=pqgotest sslmode=verify-full" 12 | name: postgres blank password 13 | username: 14 | value: pqgotest 15 | password: 16 | value: "" 17 | query: "SELECT 1" 18 | results: 1 19 | -------------------------------------------------------------------------------- /fixtures/azure/devops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: azure-devops 5 | spec: 6 | interval: 300 7 | azureDevops: 8 | - project: Demo1 9 | pipeline: ^windows- 10 | personalAccessToken: 11 | value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 12 | organization: flanksource 13 | variable: 14 | env: prod 15 | branch: 16 | - main 17 | thresholdMillis: 60000 # 60 seconds 18 | -------------------------------------------------------------------------------- /fixtures/datasources/mssql_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mssql-pass 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | mssql: 9 | - url: "server=mssql.canaries.svc.cluster.local;user id=$(username);password=$(password);port=1433;database=master" 10 | name: mssql pass 11 | username: 12 | value: sa 13 | password: 14 | value: S0m3p@sswd 15 | query: "SELECT 1" 16 | results: 1 17 | -------------------------------------------------------------------------------- /fixtures/restic/restic_without_integrity_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: restic-pass-withoutinegrity 5 | spec: 6 | interval: 30 7 | restic: 8 | - repository: s3:http://minio.minio.svc.cluster.local:9000/restic-canary-checker 9 | name: restic check 10 | password: 11 | value: S0m3p@sswd 12 | maxAge: 1h 13 | accessKey: 14 | value: minio 15 | secretKey: 16 | value: minio123 17 | -------------------------------------------------------------------------------- /test/aggregate-test/Procfile: -------------------------------------------------------------------------------- 1 | main: ../../.bin/canary-checker_osx serve --httpPort=8080 -c config/main.yaml --interval=10 --maxStatusCheckCount 10 --aggregateServers=http://localhost:8081,http://localhost:8082 --name=servermain 2 | server1: ../../.bin/canary-checker_osx serve --httpPort=8081 -c config/server1.yaml --interval=10 --maxStatusCheckCount 5 --name=server1 3 | server2: ../../.bin/canary-checker_osx serve --httpPort=8082 -c config/server2.yaml --interval=10 --maxStatusCheckCount 8 --name=server2 -------------------------------------------------------------------------------- /fixtures/minimal/exec_env_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-env 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: exec-env 9 | description: "exec with env" 10 | script: | 11 | echo -n ${FL_HELLO} ${FL_WORLD} 12 | env: 13 | - name: FL_HELLO 14 | value: "hello" 15 | - name: FL_WORLD 16 | value: "world" 17 | test: 18 | expr: 'results.stdout == "hello world"' 19 | -------------------------------------------------------------------------------- /fixtures/topology/inline-check.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: inline-check 5 | spec: 6 | type: Website 7 | icon: Application 8 | schedule: "@every 5m" 9 | components: 10 | - checks: 11 | - inline: 12 | http: 13 | - name: inline-check 14 | url: https://httpbin.demo.aws.flanksource.com/status/202 15 | responseCodes: 16 | - 202 17 | name: inline-canary 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/gitleaks/gitleaks 3 | rev: v8.16.3 4 | hooks: 5 | - id: gitleaks 6 | - repo: https://github.com/golangci/golangci-lint 7 | rev: v1.52.2 8 | hooks: 9 | - id: golangci-lint 10 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 11 | rev: 3.0.0 12 | hooks: 13 | - id: shellcheck 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.4.0 16 | hooks: 17 | - id: end-of-file-fixer 18 | - id: trailing-whitespace 19 | -------------------------------------------------------------------------------- /chart/templates/grafanaDashboard.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.grafanaDashboards }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "canary-checker.name" . }}-dashboard 6 | labels: 7 | grafana_dashboard: "1" 8 | {{- include "canary-checker.labels" . | nindent 4 }} 9 | data: 10 | canary-checker-overview.json: |- 11 | {{ .Files.Get "dashboards/Overview.json" | indent 4 }} 12 | canary-checker-details.json: |- 13 | {{ .Files.Get "dashboards/Details.json" | indent 4 }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /fixtures/datasources/mongo_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mongo-fail 5 | namespace: canaries 6 | labels: 7 | "Expected-Fail": "true" 8 | spec: 9 | interval: 30 10 | mongodb: 11 | - url: mongodb://mongo2.canaries.svc.cluster.local:27017/?authSource=admin 12 | name: mongo wrong password 13 | description: test mongo instance 14 | username: 15 | value: mongoadmin 16 | password: 17 | value: wronghere2 18 | -------------------------------------------------------------------------------- /api/external/api.go: -------------------------------------------------------------------------------- 1 | package external 2 | 3 | type Endpointer interface { 4 | GetEndpoint() string 5 | } 6 | 7 | type Describable interface { 8 | GetDescription() string 9 | GetIcon() string 10 | GetName() string 11 | GetNamespace() string 12 | GetLabels() map[string]string 13 | GetTransformDeleteStrategy() string 14 | GetMetricsSpec() []Metrics 15 | } 16 | 17 | type WithType interface { 18 | GetType() string 19 | } 20 | 21 | type Check interface { 22 | Endpointer 23 | Describable 24 | WithType 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/datasources/postgres_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-succeed 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | postgres: 9 | - url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 10 | name: postgres schemas check 11 | username: 12 | value: postgresadmin 13 | password: 14 | value: admin123 15 | query: SELECT 1 16 | results: 1 17 | -------------------------------------------------------------------------------- /fixtures/restic/restic_with_integrity_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: restic-pass-inegrity 5 | spec: 6 | interval: 30 7 | restic: 8 | - repository: s3:http://minio.minio.svc.cluster.local:9000/restic-canary-checker 9 | name: restic integrity check 10 | password: 11 | value: S0m3p@sswd 12 | maxAge: 1h 13 | accessKey: 14 | value: minio 15 | secretKey: 16 | value: minio123 17 | checkIntegrity: true 18 | -------------------------------------------------------------------------------- /fixtures/datasources/mysql_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mysql-fail 5 | namespace: canaries 6 | labels: 7 | "Expected-Fail": "true" 8 | spec: 9 | interval: 30 10 | mysql: 11 | - url: "$(username):$(password)@tcp(mysql.canaries.svc.cluster.local:3306)/mysqldb" 12 | name: mysql wrong password 13 | username: 14 | value: mysqladmin 15 | password: 16 | value: wrongpassword 17 | query: "SELECT 1" 18 | results: 1 19 | -------------------------------------------------------------------------------- /pkg/api/utils.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | ) 6 | 7 | // Deprecated: use HTTPError 8 | func errorResponse(c echo.Context, err error, code int) error { 9 | e := map[string]string{"error": err.Error()} 10 | return c.JSON(code, e) 11 | } 12 | 13 | // abs returns the absolute value of i. 14 | // math.Abs only supports float64 and this avoids the needless type conversions 15 | // and ugly expression. 16 | func abs(n int) int { 17 | if n > 0 { 18 | return n 19 | } 20 | 21 | return -n 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/datasources/GCP/folder_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: recursive-folder-check 6 | spec: 7 | interval: 30 8 | folder: 9 | - path: gcs://folder-check-test/recursive-test 10 | name: recursive folders 11 | namespace: default 12 | minCount: 3 13 | recursive: true 14 | display: 15 | expr: results.?files.orValue([]).map(i, i.name).join(", ") 16 | gcpConnection: 17 | connection: connection://gcs/flanksource-prod 18 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: kube-pass 5 | spec: 6 | interval: 30 7 | kubernetes: 8 | - namspaceSelector: 9 | name: canaries 10 | name: k8s-ready pods 11 | kind: Pod 12 | resource: 13 | labelSelector: app=k8s-ready 14 | - namspaceSelector: 15 | name: canaries 16 | kind: Pod 17 | name: k8s-ready pods 18 | ready: false 19 | resource: 20 | labelSelector: app=k8s-not-ready 21 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_connection_aws_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: aws-exec 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | exec: 10 | - name: aws-exec-check 11 | description: "exec s3 list" 12 | script: aws s3 ls | head -n 1 13 | connections: 14 | aws: 15 | connection: connection://AWS/flanksource 16 | test: 17 | expr: results.stdout == '2023-05-25 11:49:22 cf-templates-3ci8g0qv95rq-eu-west-1' 18 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth 5 | spec: 6 | http: 7 | - name: "basic auth fail" 8 | endpoint: https://httpbin.demo.aws.flanksource.com/basic-auth/hello/world 9 | responseCodes: [401] 10 | - name: "basic auth pass" 11 | endpoint: https://httpbin.demo.aws.flanksource.com/basic-auth/hello/world 12 | responseCodes: [200] 13 | username: 14 | value: hello 15 | password: 16 | value: world 17 | -------------------------------------------------------------------------------- /fixtures/topology/kube-dns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: kube-dns 5 | labels: 6 | canary: kube-dns-pods 7 | spec: 8 | type: KubernetesCluster 9 | icon: kubernetes 10 | schedule: "@every 20m" 11 | id: 12 | javascript: properties.id 13 | components: 14 | - selectors: 15 | - labelSelector: "k8s-app=kube-dns" 16 | name: kube-dns 17 | - selectors: 18 | - labelSelector: "component=kube-scheduler" 19 | name: kube-scheduler 20 | 21 | -------------------------------------------------------------------------------- /fixtures/minimal/http_fail_connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | http: 10 | - connection: 'connection://HTTP/500' 11 | name: http fail response code check 12 | responseCodes: [200] 13 | - connection: 'connection://HTTP/200' 14 | name: http fail test expr check 15 | display: 16 | expr: string(code) + " should be 500" 17 | test: 18 | expr: code == 500 19 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: canary-checker 3 | description: Kubernetes native, multi-tenant synthetic monitoring system 4 | type: application 5 | version: 0.0.0 6 | appVersion: "master" 7 | maintainers: 8 | - name: Flanksource 9 | url: https://www.flanksource.com 10 | icon: https://github.com/flanksource/docs/blob/main/docs/canary-checker/images/canary-checker-icon.svg 11 | dependencies: 12 | - name: flanksource-ui 13 | version: "1.0.651" 14 | repository: https://flanksource.github.io/charts 15 | condition: flanksource-ui.enabled 16 | -------------------------------------------------------------------------------- /chart/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: "{{if .Values.serviceAccount.rbac.clusterRole}}Cluster{{end}}RoleBinding" 3 | metadata: 4 | name: {{ include "canary-checker.fullname" . }}-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: "{{if .Values.serviceAccount.rbac.clusterRole}}Cluster{{end}}Role" 8 | name: {{ include "canary-checker.name" . }}-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: {{ .Values.serviceAccount.name }} 12 | namespace: {{ .Release.Namespace }} 13 | -------------------------------------------------------------------------------- /fixtures/elasticsearch/elasticsearch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: elasticsearch-pass 5 | spec: 6 | interval: 30 7 | elasticsearch: 8 | - url: http://elasticsearch.canaries.svc.cluster.local:9200 9 | description: Elasticsearch checker 10 | index: index 11 | query: | 12 | { 13 | "query": { 14 | "term": { 15 | "system.role": "api" 16 | } 17 | } 18 | } 19 | results: 1 20 | name: elasticsearch_pass 21 | -------------------------------------------------------------------------------- /fixtures/git/git_test_expression_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: github-test-expression-pass 5 | spec: 6 | interval: 30 7 | github: 8 | - query: "SELECT * FROM github_repo_checks('flanksource/template-operator') where branch='master'" 9 | name: github-expresion-check 10 | test: 11 | expr: 'Age(results[0]["started_at"]) > Duration("10m")' 12 | githubToken: 13 | valueFrom: 14 | secretKeyRef: 15 | name: github-token 16 | key: GITHUB_TOKEN 17 | -------------------------------------------------------------------------------- /fixtures/git/git_pull_push_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: git-pull-push 5 | namespace: canaries 6 | spec: 7 | gitProtocol: 8 | - name: git-pull-push 9 | username: 10 | valueFrom: 11 | secretKeyRef: 12 | key: username 13 | name: gitea 14 | password: 15 | valueFrom: 16 | secretKeyRef: 17 | key: password 18 | name: gitea 19 | repository: http://gitea-http.gitea:3000/gitea_admin/test_repo.git 20 | interval: 10 21 | -------------------------------------------------------------------------------- /fixtures/k8s/http_auth_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-configmap 5 | namespace: canaries 6 | spec: 7 | http: 8 | - endpoint: https://httpbin.demo.aws.flanksource.com/basic-auth/hello/world 9 | responseCodes: [200] 10 | username: 11 | valueFrom: 12 | configMapKeyRef: 13 | name: basic-auth 14 | key: user 15 | password: 16 | valueFrom: 17 | configMapKeyRef: 18 | name: basic-auth 19 | key: pass 20 | -------------------------------------------------------------------------------- /fixtures/k8s/junit_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: junit-pass 5 | spec: 6 | schedule: "@every 2h" 7 | junit: 8 | - testResults: "/tmp/junit-results/" 9 | name: junit-pass 10 | test: 11 | expr: results.failed == 0 && results.passed > 0 12 | display: 13 | expr: "string(results.failed) + ' of ' + string(results.passed)" 14 | spec: 15 | containers: 16 | - name: jes 17 | image: docker.io/tarun18/junit-test-pass 18 | command: ["/start.sh"] 19 | -------------------------------------------------------------------------------- /config/base/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: canary-checker 5 | annotations: 6 | kubernetes.io/tls-acme: "true" 7 | spec: 8 | tls: 9 | - hosts: 10 | - canary-checker 11 | secretName: canary-tls 12 | rules: 13 | - host: canary-checker 14 | http: 15 | paths: 16 | - path: / 17 | pathType: ImplementationSpecific 18 | backend: 19 | service: 20 | name: canary-checker 21 | port: 22 | number: 8080 23 | -------------------------------------------------------------------------------- /fixtures/minimal/http_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | http: 10 | - endpoint: https://httpbin.demo.aws.flanksource.com/status/500 11 | name: http fail response code check 12 | responseCodes: [200] 13 | - endpoint: https://httpbin.demo.aws.flanksource.com/status/200 14 | name: http fail test expr check 15 | display: 16 | expr: string(code) + " should be 500" 17 | test: 18 | expr: code == 500 19 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_artifact.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-artifact 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: exec-pass-with-artifact 9 | description: "exec dummy check" 10 | script: | 11 | mkdir -p /tmp/exec-results && 12 | echo "hello" > /tmp/exec-results/hello && echo "world" > /tmp/exec-results/world && echo "random" > /tmp/random-text && echo "to stdout" 13 | artifacts: 14 | - path: /tmp/exec-results/* 15 | - path: /tmp/random-text 16 | - path: /dev/stdout 17 | -------------------------------------------------------------------------------- /fixtures/aws/s3-protocol.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-protocol-check 5 | spec: 6 | interval: 30 7 | s3: 8 | - name: s3-check 9 | bucketName: flanksource-public 10 | objectPath: dummy 11 | region: us-east-1 12 | accessKey: 13 | valueFrom: 14 | secretKeyRef: 15 | name: aws-credentials 16 | key: AWS_ACCESS_KEY_ID 17 | secretKey: 18 | valueFrom: 19 | secretKeyRef: 20 | name: aws-credentials 21 | key: AWS_SECRET_ACCESS_KEY 22 | -------------------------------------------------------------------------------- /fixtures/elasticsearch/elasticsearch_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: elasticsearch-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | elasticsearch: 10 | - url: http://elasticsearch-wrong-host.example.com:9200 11 | description: Elasticsearch checker 12 | index: index 13 | query: | 14 | { 15 | "query": { 16 | "term": { 17 | "system.role": "api" 18 | } 19 | } 20 | } 21 | results: 1 22 | name: elasticsearch-fail 23 | -------------------------------------------------------------------------------- /fixtures/k8s/http_auth_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-secret 5 | namespace: canaries 6 | annotation: 7 | debug: "true" 8 | spec: 9 | http: 10 | - endpoint: https://httpbin.demo.aws.flanksource.com/basic-auth/hello/world 11 | responseCodes: [200] 12 | username: 13 | valueFrom: 14 | secretKeyRef: 15 | name: basic-auth 16 | key: user 17 | password: 18 | valueFrom: 19 | secretKeyRef: 20 | name: basic-auth 21 | key: pass 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Build 3 | # Declare default permissions as read only 4 | permissions: read-all 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | target: 12 | - docker-full 13 | - docker-minimal 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 17 | - name: Build Container 18 | run: make ${{matrix.target}} 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /checks/timer.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Timer struct { 9 | Start time.Time 10 | } 11 | 12 | func (t Timer) Elapsed() float64 { 13 | return float64(time.Since(t.Start).Milliseconds()) 14 | } 15 | 16 | func (t Timer) Millis() int64 { 17 | return time.Since(t.Start).Milliseconds() 18 | } 19 | 20 | func (t Timer) String() string { 21 | return fmt.Sprintf("%dms", t.Millis()) 22 | } 23 | func (t Timer) Duration() *time.Duration { 24 | d := time.Since(t.Start) 25 | return &d 26 | } 27 | 28 | func NewTimer() Timer { 29 | return Timer{Start: time.Now()} 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/k8s/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canaries 4 | resources: 5 | - _setup.yaml 6 | - junit_fail.yaml 7 | - junit_pass.yaml 8 | - slow/namespace_pass.yaml 9 | - pod_fail.yaml 10 | - pod_pass.yaml 11 | - cronjob_monitor_fail.yaml 12 | - cronjob_monitor.yaml 13 | - kubernetes_bundle.yaml 14 | - kubernetes_resource_ingress_pass.yaml 15 | - kubernetes_resource_namespace_pass.yaml 16 | - kubernetes_resource_pod_exit_code_pass.yaml 17 | - kubernetes_resource_service_fail.yaml 18 | - kubernetes_resource_service_pass.yaml 19 | -------------------------------------------------------------------------------- /fixtures/minimal/http-check-labels.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-checks 5 | labels: 6 | canary: http-checks 7 | spec: 8 | interval: 30 9 | http: 10 | - url: https://httpbin.demo.aws.flanksource.com/status/200 11 | name: http-pass-single 12 | labels: 13 | check: http-200 14 | responseCodes: [201, 200, 301] 15 | responseContent: "" 16 | - url: https://httpbin.demo.aws.flanksource.com/status/202 17 | name: http-pass-multiple 18 | labels: 19 | check: http-202 20 | responseCodes: [201, 202, 301] 21 | -------------------------------------------------------------------------------- /fixtures/opensearch/_post_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running kubectl wait for opensearch" 4 | kubectl -n canaries wait --for=condition=ready pod -l app=opensearch --timeout=5m 5 | 6 | echo "Fetching elastic search health"; 7 | curl -s "http://opensearch.canaries.svc.cluster.local:9200/_cluster/health" -H 'Content-Type: application/json'; 8 | curl -s "http://opensearch.canaries.svc.cluster.local:9200/_cluster/allocation/explain" -H 'Content-Type: application/json'; 9 | 10 | kubectl get pods --all-namespaces 11 | 12 | echo "Fetching populate-db logs from opensearch pod"; 13 | kubectl logs -n canaries -l app=opensearch 14 | -------------------------------------------------------------------------------- /fixtures/opensearch/opensearch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: opensearch-pass 5 | namespace: canaries 6 | labels: 7 | canary: opensearch 8 | spec: 9 | schedule: "@every 30s" 10 | opensearch: 11 | - name: opensearch_pass 12 | description: OpenSearch checker 13 | url: http://opensearch.canaries.svc.cluster.local:9200 14 | index: index 15 | query: | 16 | { 17 | "query": { 18 | "term": { 19 | "system.version": "v1.0" 20 | } 21 | } 22 | } 23 | results: 1 24 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /pkg/runner/shutdown.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/flanksource/commons/logger" 7 | ) 8 | 9 | var shutdownHooks []func() 10 | 11 | func Shutdown() { 12 | if len(shutdownHooks) == 0 { 13 | return 14 | } 15 | logger.Infof("Shutting down") 16 | for _, fn := range shutdownHooks { 17 | fn() 18 | } 19 | shutdownHooks = []func(){} 20 | } 21 | 22 | func ShutdownAndExit(code int, msg string) { 23 | Shutdown() 24 | logger.StandardLogger().WithSkipReportLevel(1).Errorf(msg) 25 | os.Exit(code) 26 | } 27 | 28 | func AddShutdownHook(fn func()) { 29 | shutdownHooks = append(shutdownHooks, fn) 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/elasticsearch/_post_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running kubectl wait for elasticsearch" 4 | kubectl -n canaries wait --for=condition=ready pod -l app=elasticsearch --timeout=5m 5 | 6 | echo "Fetching elastic search health"; 7 | curl -s "http://elasticsearch.canaries.svc.cluster.local:9200/_cluster/health" -H 'Content-Type: application/json'; 8 | curl -s "http://elasticsearch.canaries.svc.cluster.local:9200/_cluster/allocation/explain" -H 'Content-Type: application/json'; 9 | 10 | kubectl get pods --all-namespaces 11 | 12 | echo "Fetching populate-db logs from elasticsearch pod"; 13 | kubectl logs -n canaries -l app=elasticsearch 14 | -------------------------------------------------------------------------------- /fixtures/opensearch/opensearch_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: opensearch-fail 5 | namespace: canaries 6 | labels: 7 | canary: opensearch 8 | "Expected-Fail": "true" 9 | spec: 10 | schedule: "@every 30s" 11 | opensearch: 12 | - name: opensearch_fail 13 | description: OpenSearch checker 14 | url: http://opensearch.canaries.svc.cluster.local:9200 15 | index: index 16 | query: | 17 | { 18 | "query": { 19 | "term": { 20 | "system.role": "api" 21 | } 22 | } 23 | } 24 | results: 100 25 | -------------------------------------------------------------------------------- /chart/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.serviceMonitor true }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "canary-checker.name" . }}-monitor 6 | labels: 7 | {{- include "canary-checker.labels" . | nindent 4 }} 8 | spec: 9 | jobLabel: {{ include "canary-checker.name" . }} 10 | endpoints: 11 | - port: http 12 | interval: 30s 13 | honorLabels: true 14 | metricRelabelings: 15 | - action: labeldrop 16 | regex: (pod|instance) 17 | selector: 18 | matchLabels: 19 | {{- include "canary-checker.selectorLabels" . | nindent 6 }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /fixtures/minimal/http_template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: templated-http 5 | spec: 6 | interval: 30 7 | http: 8 | - name: templated-http 9 | endpoint: https://webhook.site/#!/9f1392a6-718a-4ef5-a8e2-bfb55b08afca/f93d307b-0aaf-4a38-b9b3-db5daaae5657/1 10 | responseCodes: [200] 11 | templateBody: true 12 | envVar: 13 | - name: db 14 | valueFrom: 15 | secretKeyRef: 16 | name: db-user-pass 17 | key: username 18 | body: | 19 | { 20 | "canary": "{{.canary.name}}", 21 | "secret": "{{.db}}" 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/aws/minimal/aws_exec_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: aws-exec-pass 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: aws-exec-list-s3-buckets-pass-check 9 | description: List s3 buckets 10 | script: aws s3 ls 11 | connections: 12 | aws: 13 | accessKey: 14 | valueFrom: 15 | secretKeyRef: 16 | name: aws-credentials 17 | key: AWS_ACCESS_KEY_ID 18 | secretKey: 19 | valueFrom: 20 | secretKeyRef: 21 | name: aws-credentials 22 | key: AWS_SECRET_ACCESS_KEY 23 | -------------------------------------------------------------------------------- /checks/folder_test.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | v1 "github.com/flanksource/canary-checker/api/v1" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestFolderFilterSinceMath(t *testing.T) { 12 | RegisterTestingT(t) 13 | ctx, err := v1.FolderFilter{ 14 | Since: "now-1h", 15 | }.New() 16 | 17 | Expect(err).ToNot(HaveOccurred()) 18 | Expect(*ctx.Since).To(BeTemporally("~", time.Now().Add(-1*time.Hour), 1*time.Second)) 19 | } 20 | 21 | func TestFolderFilterSinceParse(t *testing.T) { 22 | RegisterTestingT(t) 23 | _, err := v1.FolderFilter{ 24 | Since: "2023-10-31T19:18:57.14974Z", 25 | }.New() 26 | 27 | Expect(err).ToNot(HaveOccurred()) 28 | } 29 | -------------------------------------------------------------------------------- /fixtures/git/gitea.values: -------------------------------------------------------------------------------- 1 | gitea: 2 | additionalConfigFromEnvs: [] 3 | additionalConfigSources: [] 4 | admin: 5 | email: gitea@local.domain 6 | existingSecret: null 7 | password: admin 8 | username: gitea_admin 9 | config: 10 | security: 11 | PASSWORD_COMPLEXITY: "off" 12 | server: 13 | SSH_LISTEN_PORT: 2222 14 | SSH_PORT: 22 15 | database: 16 | DB_TYPE: sqlite3 17 | session: 18 | PROVIDER: memory 19 | cache: 20 | ADAPTER: memory 21 | queue: 22 | TYPE: level 23 | 24 | persistence: 25 | enabled: false 26 | postgresql-ha: 27 | enabled: false 28 | redis-cluster: 29 | enabled: false 30 | postgresql: 31 | enabled: false 32 | -------------------------------------------------------------------------------- /fixtures/k8s/certmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: cert-manager 5 | spec: 6 | schedule: "@every 15m" 7 | kubernetes: 8 | - name: cert-manager-check 9 | kind: Certificate 10 | test: 11 | expr: | 12 | dyn(results). 13 | map(i, i.Object). 14 | filter(i, i.status.conditions[0].status != "True").size() == 0 15 | display: 16 | expr: | 17 | dyn(results). 18 | map(i, i.Object). 19 | filter(i, i.status.conditions[0].status != "True"). 20 | map(i, "%s/%s -> %s".format([i.metadata.namespace, i.metadata.name, i.status.conditions[0].message])).join('\n') 21 | -------------------------------------------------------------------------------- /fixtures/quarantine/smb_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: smb-pass 5 | spec: 6 | interval: 30 7 | folder: 8 | # Check for any backup not older than 7 days and min size 25 bytes 9 | - path: \\windows-server\sharename\folder 10 | smbConnection: 11 | username: 12 | valueFrom: 13 | secretKeyRef: 14 | name: smb-credentials 15 | key: USERNAME 16 | password: 17 | valueFrom: 18 | secretKeyRef: 19 | name: ssmb-credentials 20 | key: PASSWORD 21 | filter: 22 | regex: "(.*)backup.zip$" 23 | maxAge: 7d 24 | minSize: 25b 25 | -------------------------------------------------------------------------------- /pkg/api/details.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/flanksource/canary-checker/pkg/cache" 7 | "github.com/flanksource/commons/logger" 8 | "github.com/labstack/echo/v4" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func DetailsHandler(c echo.Context) error { 13 | queryParams := c.Request().URL.Query() 14 | key := queryParams.Get("key") 15 | time := queryParams.Get("time") 16 | if key == "" || time == "" { 17 | logger.Errorf("key and time are required parameters") 18 | return errorResponse(c, errors.New("key and time are required parameters"), http.StatusBadRequest) 19 | } 20 | detail := cache.PostgresCache.GetDetails(key, time) 21 | return c.JSON(http.StatusOK, detail) 22 | } 23 | -------------------------------------------------------------------------------- /fixtures/topology/selector.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: selector 5 | spec: 6 | type: KubernetesCluster 7 | icon: kubernetes 8 | schedule: "@every 20m" 9 | id: 10 | javascript: properties.id 11 | components: 12 | # - pods: 13 | # k8s-app: kube-dns 14 | - selectors: 15 | - labelSelector: "namespace=kube-system" 16 | - canarySelector: 17 | - labelSelector: "canary=http-check" 18 | - inline: 19 | - http: 20 | url: https://httpbin.demo.aws.flanksource.com/status/200 21 | test: 22 | expr: "code == 200" 23 | name: selector 24 | type: aggregator 25 | -------------------------------------------------------------------------------- /test/karina.yaml: -------------------------------------------------------------------------------- 1 | versions: 2 | kind: 0.18.0 3 | patches: 4 | - ./patch1.yaml 5 | domain: 127.0.0.1.nip.io 6 | ca: 7 | cert: ../.certs/root-ca.crt 8 | privateKey: ../.certs/root-ca.key 9 | password: foobar 10 | ingressCA: 11 | cert: ../.certs/ingress-ca.crt 12 | privateKey: ../.certs/ingress-ca.key 13 | password: foobar 14 | kubernetes: 15 | version: v1.20.7 16 | kubeletExtraArgs: 17 | node-labels: "ingress-ready=true" 18 | authorization-mode: "AlwaysAllow" 19 | podSubnet: 100.200.0.0/16 20 | serviceSubnet: 100.100.0.0/16 21 | templateOperator: 22 | disabled: true 23 | dex: 24 | disabled: true 25 | quack: 26 | disabled: true 27 | calico: 28 | ipip: Never 29 | vxlan: Never 30 | version: v3.8.2 31 | -------------------------------------------------------------------------------- /test/aggregate-test/config/server1.yaml: -------------------------------------------------------------------------------- 1 | dns: 2 | - server: 8.8.8.8 3 | port: 53 4 | query: "flanksource.com" 5 | querytype: "A" 6 | minrecords: 1 7 | exactreply: ["8.8.8.8"] 8 | timeout: 10 9 | - server: 8.8.8.8 10 | port: 53 11 | query: "1.2.3.4.nip.io" 12 | querytype: "A" 13 | minrecords: 1 14 | exactreply: ["1.2.3.4"] 15 | timeout: 10 16 | # docker: 17 | # - image: docker.io/library/busybox:1.31.1 18 | # username: 19 | # password: 20 | # expectedDigest: 6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a 21 | # expectedSize: 1219782 22 | # - image: docker.io/library/busybox:random 23 | # username: 24 | # password: 25 | # expectedDigest: abcdef123 26 | # expectedSize: 200 -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-minimal_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: kube-system-checks 5 | spec: 6 | interval: 30 7 | kubernetes: 8 | - namespace: kube-system 9 | name: kube-system 10 | kind: Pod 11 | # ready: true 12 | # resource: 13 | # labelSelector: k8s-app=kube-dns 14 | namespaceSelector: 15 | name: default 16 | display: 17 | expr: | 18 | dyn(results). 19 | map(i, i.Object). 20 | filter(i, !k8s.isHealthy(i)). 21 | map(i, "%s/%s -> %s".format([i.metadata.namespace, i.metadata.name, k8s.getHealth(i).message])).join('\n') 22 | test: 23 | expr: dyn(results).all(x, k8s.isHealthy(x)) 24 | -------------------------------------------------------------------------------- /fixtures/minimal/metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-pass-single 5 | spec: 6 | interval: 30 7 | http: 8 | - name: http-minimal-check 9 | url: https://httpbin.demo.aws.flanksource.com/status/200 10 | metrics: 11 | - name: httpbin_count 12 | type: counter 13 | value: "1" 14 | labels: 15 | - name: check_name 16 | valueExpr: check.name 17 | - name: code 18 | valueExpr: code 19 | - name: httpbin_2xx_duration 20 | type: counter 21 | value: elapsed.getMilliseconds() 22 | labels: 23 | - name: check_name 24 | valueExpr: check.name 25 | -------------------------------------------------------------------------------- /fixtures/aws/cloudwatch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: cloudwatch 5 | spec: 6 | interval: 30 7 | cloudwatch: 8 | region: "eu-west-1" 9 | transform: 10 | expr: | 11 | results.MetricAlarms.filter(i, i.StateValue != 'OK' ).map(i, 12 | { 13 | 'name': i.MetricName, 14 | 'icon': 'aws-cloudwatch-alarm', 15 | 'duration': time.Since(timestamp(i.StateTransitionedTimestamp)).getMilliseconds(), 16 | 'labels': fromAWSMap(i.Dimensions). 17 | merge({'arn': i.AlarmArn}), 18 | 'message': "%s (%s) %s".format([i.AlarmDescription, i.AlarmArn, fromAWSMap(i.Dimensions)]), 19 | } 20 | ).toJSON() 21 | -------------------------------------------------------------------------------- /fixtures/ldap/ldap_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: ldap-pass 5 | spec: 6 | interval: 30 7 | ldap: 8 | - url: ldap://apacheds.ldap.svc.cluster.local:10389 9 | name: ldap user login 10 | username: 11 | value: uid=admin,ou=system 12 | password: 13 | value: secret 14 | bindDN: ou=users,dc=example,dc=com 15 | userSearch: "(&(objectClass=organizationalPerson))" 16 | - url: ldap://apacheds.ldap.svc.cluster.local:10389 17 | name: ldap group login 18 | username: 19 | value: uid=admin,ou=system 20 | password: 21 | value: secret 22 | bindDN: ou=groups,dc=example,dc=com 23 | userSearch: "(&(objectClass=groupOfNames))" 24 | -------------------------------------------------------------------------------- /fixtures/external/alertmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: alert-manager-webhook-check 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | # schedule: "@every 1m" # (Not required for webhook checks) 9 | webhook: 10 | name: my-webhook 11 | token: 12 | value: webhook-auth-token 13 | transform: 14 | expr: | 15 | results.json.alerts.map(r, 16 | { 17 | 'name': r.name + r.fingerprint, 18 | 'labels': r.labels, 19 | 'icon': 'alert', 20 | 'message': r.annotations.summary, 21 | 'description': r.annotations.description, 22 | 'deletedAt': has(r.endsAt) ? r.endsAt : null, 23 | } 24 | ).toJSON() 25 | -------------------------------------------------------------------------------- /pkg/cache/postgres_util.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func ConvertNamedParamsDebug(sql string, namedArgs map[string]interface{}) string { 9 | // Loop the named args and replace with placeholders 10 | for pname, pval := range namedArgs { 11 | sql = strings.ReplaceAll(sql, ":"+pname, fmt.Sprintf("%v", pval)) 12 | } 13 | return sql 14 | } 15 | 16 | func ConvertNamedParams(sql string, namedArgs map[string]interface{}) (string, []interface{}) { 17 | i := 1 18 | var args []interface{} 19 | // Loop the named args and replace with placeholders 20 | for pname, pval := range namedArgs { 21 | sql = strings.ReplaceAll(sql, ":"+pname, fmt.Sprint(`$`, i)) 22 | args = append(args, pval) 23 | i++ 24 | } 25 | return sql, args 26 | } 27 | -------------------------------------------------------------------------------- /fixtures/minimal/http_pass_results_mode_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-pass 5 | spec: 6 | resultMode: "junit" 7 | interval: 30 8 | http: 9 | - url: https://httpbin.demo.aws.flanksource.com/status/200 10 | name: http pass response 200 status code 11 | thresholdMillis: 30000 12 | responseCodes: [201, 301, 200] 13 | responseContent: "" 14 | maxSSLExpiry: 7 15 | description: "HTTP dummy test 2" 16 | - url: https://httpbin.demo.aws.flanksource.com/status/201 17 | name: http pass response 201 status code 18 | thresholdMillis: 30000 19 | responseCodes: [201] 20 | responseContent: "" 21 | maxSSLExpiry: 7 22 | description: "second http check here" 23 | -------------------------------------------------------------------------------- /api/external/metrics.go: -------------------------------------------------------------------------------- 1 | package external 2 | 3 | // +kubebuilder:object:generate=true 4 | type Metrics struct { 5 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 6 | Labels MetricLabels `json:"labels,omitempty" yaml:"labels,omitempty"` 7 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 8 | Value string `json:"value,omitempty" yaml:"value,omitempty"` 9 | } 10 | 11 | type MetricLabels []MetricLabel 12 | 13 | type MetricLabel struct { 14 | Name string `json:"name"` 15 | Value string `json:"value,omitempty"` 16 | ValueExpr string `json:"valueExpr,omitempty"` 17 | } 18 | 19 | func (labels MetricLabels) Names() []string { 20 | var names []string 21 | for _, k := range labels { 22 | names = append(names, k.Name) 23 | } 24 | return names 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/aws/s3_bucket_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-bucket-pass 5 | annotations: 6 | trace: "false" 7 | spec: 8 | interval: 30 9 | folder: 10 | # Check for any backup not older than 7 days and min size 25 bytes 11 | - path: s3://flanksource-public 12 | awsConnection: 13 | region: eu-central-1 14 | minSize: 50M 15 | maxAge: 10d 16 | filter: 17 | regex: .*.ova 18 | minSize: 100M 19 | # maxAge: 18760h 20 | display: 21 | template: | 22 | {{- range $f := .results.Files }} 23 | {{- if gt $f.Size 0 }} 24 | Name: {{$f.Name}} {{$f.ModTime | humanizeTime }} {{ $f.Size | humanizeBytes}} 25 | {{- end}} 26 | {{- end }} 27 | -------------------------------------------------------------------------------- /fixtures/external/dynatrace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: dynatrace 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 1m" 9 | owner: DBAdmin 10 | severity: high 11 | dynatrace: 12 | - name: dynatrace 13 | scheme: https 14 | host: 15 | apiKey: 16 | value: '' # https://www.dynatrace.com/support/help/manage/access-control/access-tokens/personal-access-token 17 | javascript: | 18 | var out = _.map(results, function(r) { 19 | return { 20 | name: r.title, 21 | description: r.title, 22 | labels: r.labels, 23 | severity: r.severity, 24 | } 25 | }) 26 | JSON.stringify(out); 27 | -------------------------------------------------------------------------------- /cmd/sync.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | configSync "github.com/flanksource/canary-checker/pkg/sync" 7 | "github.com/flanksource/commons/logger" 8 | ) 9 | 10 | var Sync = &cobra.Command{ 11 | Use: "sync", 12 | } 13 | 14 | var AddCanary = &cobra.Command{ 15 | Use: "canary ", 16 | Short: "Add a new canary spec", 17 | Run: func(cmd *cobra.Command, configFiles []string) { 18 | 19 | if ctx, err := InitContext(); err != nil { 20 | logger.Fatalf("error connecting with postgres %v", err) 21 | } else { 22 | if err := configSync.SyncCanary(ctx, dataFile, configFiles...); err != nil { 23 | logger.Fatalf("Could not sync canaries: %v", err) 24 | } 25 | } 26 | }, 27 | } 28 | 29 | func init() { 30 | Sync.AddCommand(AddCanary) 31 | Root.AddCommand(Sync) 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/aws/aws_config_rule_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: awsconfigrule-pass 5 | spec: 6 | interval: 30 7 | awsConfigRule: 8 | - name: AWS Config Rule 9 | region: "eu-west-1" 10 | complianceTypes: [NON_COMPLIANT] 11 | transform: 12 | expr: | 13 | results.rules.map(i, 14 | i.resources.map(r, 15 | { 16 | 'name': i.rule + "/" + r.type + "/" + r.id, 17 | 'description': i.rule, 18 | 'icon': 'aws-config-alarm', 19 | 'duration': time.Since(timestamp(r.recorded)).getMilliseconds(), 20 | 'labels': {'id': r.id, 'type': r.type}, 21 | 'message': i.description + i.annotation + r.annotation 22 | }) 23 | ).flatten().toJSON() 24 | -------------------------------------------------------------------------------- /fixtures/aws/aws_config_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: awsconfig-pass 5 | spec: 6 | interval: 30 7 | awsConfig: 8 | - query: | 9 | SELECT 10 | configuration.complianceType, 11 | COUNT(*) 12 | WHERE 13 | resourceType = 'AWS::Config::ResourceCompliance' 14 | GROUP BY 15 | configuration.complianceType 16 | awsConnection: 17 | accessKeyID: 18 | valueFrom: 19 | secretKeyRef: 20 | name: aws-credentials 21 | key: AWS_ACCESS_KEY_ID 22 | secretKey: 23 | valueFrom: 24 | secretKeyRef: 25 | name: aws-credentials 26 | key: AWS_SECRET_ACCESS_KEY 27 | region: af-south-1 28 | display: 29 | template: "{{ .results }}" -------------------------------------------------------------------------------- /fixtures/restic/_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | curl -sSLo /usr/local/bin/restic.bz2 https://github.com/restic/restic/releases/download/v0.12.1/restic_0.12.1_$(OS)_$(ARCH).bz2 && \ 6 | bunzip2 /usr/local/bin/restic.bz2 && \ 7 | chmod +x /usr/local/bin/restic 8 | 9 | 10 | restic version 11 | # Initialize Restic Repo 12 | # Do not fail if it already exists 13 | RESTIC_PASSWORD="S0m3p@sswd" AWS_ACCESS_KEY_ID="minio" AWS_SECRET_ACCESS_KEY="minio123" restic --cacert .certs/ingress-ca.crt -r s3:https://minio.${DOMAIN}/restic-canary-checker init || true 14 | #take some backup in restic 15 | RESTIC_PASSWORD="S0m3p@sswd" AWS_ACCESS_KEY_ID="minio" AWS_SECRET_ACCESS_KEY="minio123" restic --cacert .certs/ingress-ca.crt -r s3:https://minio.${DOMAIN}/restic-canary-checker backup "$(pwd)" 16 | #Sleep for 5 seconds for restic to be ready 17 | sleep 5 18 | -------------------------------------------------------------------------------- /fixtures/k8s/pod_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: pod-pass 5 | spec: 6 | interval: 900 7 | pod: 8 | - name: golang 9 | spec: | 10 | apiVersion: v1 11 | kind: Pod 12 | metadata: 13 | name: hello-world-golang 14 | labels: 15 | app: hello-world-golang 16 | spec: 17 | containers: 18 | - name: hello 19 | image: quay.io/toni0/hello-webserver-golang:latest 20 | port: 8080 21 | path: /foo/bar 22 | scheduleTimeout: 20000 23 | readyTimeout: 10000 24 | httpTimeout: 7000 25 | deleteTimeout: 12000 26 | ingressTimeout: 10000 27 | deadline: 60000 28 | httpRetryInterval: 1500 29 | expectedContent: bar 30 | expectedHttpStatuses: [200, 201, 202] 31 | -------------------------------------------------------------------------------- /fixtures/minimal/metrics-transformed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exchange-rates 5 | annotations: 6 | trace: "true" 7 | spec: 8 | schedule: "every 30 @minutes" 9 | http: 10 | - name: exchange-rates 11 | url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS 12 | transform: 13 | expr: | 14 | { 15 | 'metrics': json.rates.keys().map(k, { 16 | 'name': "exchange_rate", 17 | 'type': "gauge", 18 | 'value': json.rates[k], 19 | 'labels': { 20 | "from": json.base, 21 | "to": k 22 | } 23 | }) 24 | }.toJSON() 25 | metrics: 26 | - name: exchange_rate_api 27 | type: histogram 28 | value: elapsed.getMilliseconds() 29 | -------------------------------------------------------------------------------- /fixtures/k8s/pod_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: pod-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 900 9 | pod: 10 | - name: fail 11 | spec: | 12 | apiVersion: v1 13 | kind: Pod 14 | metadata: 15 | name: hello-world-fail 16 | labels: 17 | app: hello-world-fail 18 | spec: 19 | containers: 20 | - name: httpbin 21 | image: kennethreitz/httpbin 22 | port: 80 23 | path: /status/500 24 | scheduleTimeout: 2000 25 | readyTimeout: 5000 26 | httpTimeout: 2000 27 | deleteTimeout: 12000 28 | ingressTimeout: 5000 29 | deadline: 100000 30 | httpRetryInterval: 1500 31 | expectedContent: '' 32 | expectedHttpStatuses: [200, 201, 202] 33 | -------------------------------------------------------------------------------- /fixtures/datasources/posgres_stateful_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-succeed 5 | namespace: canaries 6 | spec: 7 | interval: 30 8 | postgres: 9 | - name: postgres processes new 10 | url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 11 | username: 12 | value: postgresadmin 13 | password: 14 | value: admin123 15 | query: | 16 | select max(backend_start), count(*) from pg_stat_activity WHERE backend_start > 17 | {{- if last_result.results.rows }} 18 | '{{- (index last_result.results.rows 0).max }}' 19 | {{- else}} 20 | now() - interval '1 hour' 21 | {{- end}} 22 | metrics: 23 | - name: postgres_process_new 24 | type: counter 25 | value: results.rows[0].count 26 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/pkg/prometheus" 5 | "github.com/flanksource/commons/collections" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | var RunnerName string 10 | 11 | var Version string 12 | 13 | var RunnerLabels map[string]string = make(map[string]string) 14 | 15 | var Prometheus *prometheus.PrometheusClient 16 | 17 | var IncludeNamespaces []string 18 | 19 | var WatchNamespace string 20 | 21 | var IncludeCanaries []string 22 | 23 | var IncludeTypes []string 24 | 25 | func IsCanaryIgnored(canary *metav1.ObjectMeta) bool { 26 | if !collections.MatchItems(canary.Namespace, IncludeNamespaces...) { 27 | return true 28 | } 29 | 30 | if !collections.MatchItems(canary.Name, IncludeCanaries...) { 31 | return true 32 | } 33 | 34 | return canary.Annotations != nil && canary.Annotations["suspend"] == "true" 35 | } 36 | -------------------------------------------------------------------------------- /fixtures/k8s/_karina.yaml: -------------------------------------------------------------------------------- 1 | configFrom: 2 | - file: ../../test/karina.yaml 3 | ldap: 4 | adminGroup: NA1 5 | username: uid=admin,ou=system 6 | password: secret 7 | port: 10636 8 | host: apacheds.ldap 9 | userDN: ou=users,dc=example,dc=com 10 | groupDN: ou=groups,dc=example,dc=com 11 | groupObjectClass: groupOfNames 12 | groupNameAttr: DN 13 | e2e: 14 | mock: true 15 | username: test 16 | password: secret 17 | s3: 18 | endpoint: http://minio.minio.svc.cluster.local:9000 19 | access_key: minio 20 | secret_key: minio123 21 | region: us-east1 22 | usePathStyle: true 23 | skipTLSVerify: true 24 | minio: 25 | version: RELEASE.2020-09-02T18-19-50Z 26 | access_key: minio 27 | secret_key: minio123 28 | replicas: 1 29 | monitoring: 30 | disabled: false 31 | grafana: 32 | # skipDashboards: true 33 | disabled: true 34 | prometheus: 35 | persistence: 36 | capacity: 2Gi 37 | -------------------------------------------------------------------------------- /fixtures/topology/component-with-parent-lookup.yml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: test-topology-with-parent-lookup 5 | spec: 6 | schedule: "@every 10m" 7 | components: 8 | - name: Parent-1 9 | type: Type1 10 | components: 11 | - name: Child-1A 12 | - name: Child-1B 13 | - name: Child-1C 14 | parentLookup: 15 | name: Parent-2 16 | type: Type2 17 | - name: Child-1D 18 | parentLookup: 19 | name: Parent-3 20 | type: Type3 21 | namespace: parent3-namespace 22 | 23 | - name: Parent-2 24 | type: Type2 25 | components: 26 | - name: Child-2A 27 | - name: Child-2B 28 | - name: Child-2C 29 | parentLookup: 30 | externalID: parent-3-external-id 31 | 32 | - name: Parent-3 33 | type: Type3 34 | namespace: parent3-namespace 35 | externalID: parent-3-external-id 36 | -------------------------------------------------------------------------------- /fixtures/datasources/_karina.yaml: -------------------------------------------------------------------------------- 1 | configFrom: 2 | - file: ../../test/karina.yaml 3 | ldap: 4 | adminGroup: NA1 5 | username: uid=admin,ou=system 6 | password: secret 7 | port: 10636 8 | host: apacheds.ldap 9 | userDN: ou=users,dc=example,dc=com 10 | groupDN: ou=groups,dc=example,dc=com 11 | groupObjectClass: groupOfNames 12 | groupNameAttr: DN 13 | e2e: 14 | mock: true 15 | username: test 16 | password: secret 17 | s3: 18 | endpoint: http://minio.minio.svc.cluster.local:9000 19 | access_key: minio 20 | secret_key: minio123 21 | region: us-east1 22 | usePathStyle: true 23 | skipTLSVerify: true 24 | minio: 25 | version: RELEASE.2020-09-02T18-19-50Z 26 | access_key: minio 27 | secret_key: minio123 28 | replicas: 1 29 | monitoring: 30 | disabled: false 31 | grafana: 32 | disabled: true 33 | # skipDashboards: true 34 | prometheus: 35 | persistence: 36 | capacity: 2Gi 37 | -------------------------------------------------------------------------------- /pkg/sync/topology.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/flanksource/canary-checker/pkg" 7 | "github.com/flanksource/canary-checker/pkg/topology" 8 | "github.com/flanksource/duty/context" 9 | "github.com/friendsofgo/errors" 10 | ) 11 | 12 | func SyncTopology(ctx context.Context, dataFile string, configFiles ...string) error { 13 | if len(configFiles) == 0 { 14 | return fmt.Errorf("must specify at least one topology definition") 15 | } 16 | for _, configfile := range configFiles { 17 | configs, err := pkg.ParseTopology(configfile, dataFile) 18 | if err != nil { 19 | return errors.Wrapf(err, "could not parse %s", configfile) 20 | } 21 | 22 | for _, config := range configs { 23 | if _, history, err := topology.Run(ctx, *config); err != nil { 24 | return err 25 | } else if history.AsError() != nil { 26 | return history.AsError() 27 | } 28 | } 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis, e.g. 30s, 5m, default is 1m 3 | timeout: 20m 4 | tests: false 5 | 6 | linters: 7 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 8 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 9 | disable-all: true 10 | presets: 11 | # - bugs 12 | enable: 13 | - bodyclose 14 | - dogsled 15 | - errcheck 16 | - exportloopref 17 | - reassign 18 | - nosprintfhostport 19 | - goconst 20 | - gofmt 21 | - goimports 22 | - goprintffuncname 23 | - gosimple 24 | - govet 25 | - ineffassign 26 | - misspell 27 | - nakedret 28 | - rowserrcheck 29 | - staticcheck 30 | - stylecheck 31 | - typecheck 32 | - unconvert 33 | - unparam 34 | - unused 35 | - whitespace 36 | 37 | linters-settings: 38 | gofmt: 39 | simplify: false 40 | -------------------------------------------------------------------------------- /fixtures/minimal/namespaced_check_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: namespaced-http-check 5 | namespace: default 6 | spec: 7 | interval: 30 8 | http: 9 | - url: https://example.com 10 | name: first-namespaced-check 11 | description: "demonstrate that you can set the namespace directly on the check" 12 | namespace: dev 13 | responseCodes: [200] 14 | - url: https://example.com 15 | name: second-check 16 | description: "demonstrate that you can override the check's namespace after transformation" 17 | responseCodes: [200] 18 | transform: 19 | expr: | 20 | { 21 | 'name': 'second-after-transformation-check', 22 | 'namespace': 'prod', 23 | 'message': 'static message', 24 | 'description': 'static description', 25 | 'pass': true, 26 | }.toJSON() 27 | -------------------------------------------------------------------------------- /checks/folder_s3_test.go: -------------------------------------------------------------------------------- 1 | //go:build !fast 2 | 3 | package checks 4 | 5 | import "testing" 6 | 7 | func Test_parseS3Path(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | fullpath string 11 | wantBucket string 12 | wantPath string 13 | }{ 14 | {name: "basic", fullpath: "s3://mybucket/developers", wantBucket: "mybucket", wantPath: "developers"}, 15 | {name: "basic", fullpath: "s3://mybucket", wantBucket: "mybucket", wantPath: ""}, 16 | {name: "basic", fullpath: "mybucket", wantBucket: "mybucket", wantPath: ""}, 17 | } 18 | 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | gotBucket, gotPath := parseS3Path(tt.fullpath) 22 | if gotBucket != tt.wantBucket { 23 | t.Errorf("parseS3Path() gotBucket = %v, want %v", gotBucket, tt.wantBucket) 24 | } 25 | if gotPath != tt.wantPath { 26 | t.Errorf("parseS3Path() gotPath = %v, want %v", gotPath, tt.wantPath) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /checks/mysql.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | 6 | "github.com/flanksource/canary-checker/api/external" 7 | v1 "github.com/flanksource/canary-checker/api/v1" 8 | "github.com/flanksource/canary-checker/pkg" 9 | _ "github.com/go-sql-driver/mysql" // Necessary for mysql 10 | ) 11 | 12 | type MysqlChecker struct{} 13 | 14 | func (c *MysqlChecker) Type() string { 15 | return "mysql" 16 | } 17 | 18 | // Run: Check every entry from config according to Checker interface 19 | // Returns check result and metrics 20 | func (c *MysqlChecker) Run(ctx *context.Context) pkg.Results { 21 | var results pkg.Results 22 | for _, conf := range ctx.Canary.Spec.Mysql { 23 | results = append(results, c.Check(ctx, conf)...) 24 | } 25 | return results 26 | } 27 | 28 | func (c *MysqlChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 29 | return CheckSQL(ctx, extConfig.(v1.MysqlCheck)) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/sync/sync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/flanksource/canary-checker/pkg" 9 | "github.com/flanksource/canary-checker/pkg/db" 10 | "github.com/flanksource/commons/logger" 11 | "github.com/flanksource/duty/context" 12 | ) 13 | 14 | func SyncCanary(ctx context.Context, dataFile string, configFiles ...string) error { 15 | if len(configFiles) == 0 { 16 | return errors.New("No config file specified, running in read-only mode") 17 | } 18 | for _, configfile := range configFiles { 19 | logger.Infof("Syncing canary config %s", configfile) 20 | configs, err := pkg.ParseConfig(configfile, dataFile) 21 | if err != nil { 22 | return errors.Wrapf(err, "could not parse %s", configfile) 23 | } 24 | 25 | for _, canary := range configs { 26 | _, _, err := db.PersistCanary(ctx, canary, path.Base(configfile)) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | 13 | - package-ecosystem: docker 14 | directory: /build/dev 15 | schedule: 16 | interval: daily 17 | 18 | - package-ecosystem: docker 19 | directory: /build/full 20 | schedule: 21 | interval: daily 22 | 23 | - package-ecosystem: docker 24 | directory: /build/minimal 25 | schedule: 26 | interval: daily 27 | 28 | # - package-ecosystem: gomod 29 | # directory: /fixtures/datasources 30 | # schedule: 31 | # interval: daily 32 | 33 | # - package-ecosystem: gomod 34 | # directory: /hack/generate-schemas 35 | # schedule: 36 | # interval: daily 37 | 38 | - package-ecosystem: gomod 39 | directory: /sdk 40 | schedule: 41 | interval: daily 42 | -------------------------------------------------------------------------------- /fixtures/topology/canary-selector.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: canary-selector 5 | labels: 6 | canary: canary-selector 7 | spec: 8 | type: Website 9 | icon: Application 10 | schedule: "@every 5m" 11 | 12 | components: 13 | - checks: 14 | - selector: 15 | labelSelector: "canary=http" 16 | inline: 17 | schedule: "@every 1m" 18 | http: 19 | - name: http-pass 20 | url: https://httpbin.demo.aws.flanksource.com/status/202 21 | responseCodes: 22 | - 202 23 | name: http-component-canary 24 | - checks: 25 | - inline: 26 | schedule: "@every 1m" 27 | http: 28 | - name: http-202 29 | url: https://httpbin.demo.aws.flanksource.com/status/202 30 | responseCodes: 31 | - 202 32 | name: second-inline-canary 33 | -------------------------------------------------------------------------------- /fixtures/k8s/slow/namespace_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: namespace-pass 5 | spec: 6 | interval: 30 7 | namespace: 8 | - name: check 9 | namespaceNamePrefix: "test-foo-" 10 | podSpec: | 11 | apiVersion: v1 12 | kind: Pod 13 | metadata: 14 | name: test-namespace 15 | labels: 16 | app: hello-world-golang 17 | spec: 18 | containers: 19 | - name: hello 20 | image: quay.io/toni0/hello-webserver-golang:latest 21 | port: 8080 22 | path: /foo/bar 23 | ingressName: test-namespace-pod 24 | ingressHost: "test-namespace-pod.127.0.0.1.nip.io" 25 | readyTimeout: 5000 26 | httpTimeout: 40000 27 | deleteTimeout: 12000 28 | ingressTimeout: 40000 29 | deadline: 60000 30 | httpRetryInterval: 1500 31 | expectedContent: bar 32 | expectedHttpStatuses: [200, 201, 202] 33 | -------------------------------------------------------------------------------- /chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "canary-checker.name" . -}} 3 | apiVersion: networking.k8s.io/v1 4 | kind: Ingress 5 | metadata: 6 | name: {{ $fullName }} 7 | labels: 8 | {{- include "canary-checker.labels" . | nindent 4 }} 9 | {{- with .Values.ingress.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | {{- if .Values.ingress.tls }} 15 | tls: 16 | {{- range .Values.ingress.tls }} 17 | - hosts: 18 | {{- range .hosts }} 19 | - {{ . | quote }} 20 | {{- end }} 21 | secretName: {{ .secretName }} 22 | {{- end }} 23 | {{- end }} 24 | rules: 25 | - host: {{ .Values.ingress.host | quote }} 26 | http: 27 | paths: 28 | - path: / 29 | pathType: ImplementationSpecific 30 | backend: 31 | service: 32 | name: {{ $fullName }} 33 | port: 34 | number: 8080 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /fixtures/k8s/cronjob_monitor.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: always-passing 6 | namespace: canaries 7 | spec: 8 | schedule: "0 * * * *" 9 | concurrencyPolicy: Forbid 10 | successfulJobsHistoryLimit: 1 11 | jobTemplate: 12 | spec: 13 | backoffLimit: 1 14 | template: 15 | spec: 16 | containers: 17 | - name: fail 18 | image: busybox:1.28 19 | imagePullPolicy: IfNotPresent 20 | command: 21 | - /bin/sh 22 | - -c 23 | - exit 0 # always fail 24 | restartPolicy: OnFailure 25 | --- 26 | apiVersion: canaries.flanksource.com/v1 27 | kind: Canary 28 | metadata: 29 | name: monitor-always-passing-job 30 | spec: 31 | schedule: "@every 1m" 32 | kubernetes: 33 | - name: "Monitor always-passing job" 34 | kind: CronJob 35 | namespaceSelector: 36 | name: canaries 37 | resource: 38 | name: always-passing 39 | healthy: true 40 | -------------------------------------------------------------------------------- /pkg/topology/utils.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import "strings" 4 | 5 | func isComponent(s map[string]interface{}) bool { 6 | _, name := s["name"] 7 | _, properties := s["properties"] 8 | return name && properties 9 | } 10 | 11 | func isProperty(s map[string]interface{}) bool { 12 | _, name := s["name"] 13 | _, properties := s["properties"] 14 | return name && !properties 15 | } 16 | 17 | func isPropertyList(data []byte) bool { 18 | var s = []map[string]interface{}{} 19 | if err := json.Unmarshal(data, &s); err != nil { 20 | return false 21 | } 22 | if len(s) == 0 { 23 | return false 24 | } 25 | return isProperty(s[0]) 26 | } 27 | 28 | func isComponentList(data []byte) bool { 29 | var s = []map[string]interface{}{} 30 | if err := json.Unmarshal(data, &s); err != nil { 31 | return false 32 | } 33 | if len(s) == 0 { 34 | return false 35 | } 36 | return isComponent(s[0]) 37 | } 38 | 39 | func genParentKey(name, _type, namespace string) string { 40 | return strings.Join([]string{"parent.key", name, _type, namespace}, "/") 41 | } 42 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_namespace_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: namespace-creation 6 | namespace: default 7 | labels: 8 | "Expected-Fail": "false" 9 | spec: 10 | schedule: "@every 5m" 11 | kubernetesResource: 12 | - name: "namespace creation" 13 | namespace: "default" 14 | description: "create a namespace and pod in it" 15 | waitFor: 16 | timeout: 3m 17 | delete: true 18 | staticResources: 19 | - apiVersion: v1 20 | kind: Namespace 21 | metadata: 22 | name: test 23 | resources: 24 | - apiVersion: v1 25 | kind: Pod 26 | metadata: 27 | name: httpbin 28 | namespace: test 29 | labels: 30 | app: httpbin 31 | spec: 32 | containers: 33 | - name: httpbin 34 | image: "kennethreitz/httpbin:latest" 35 | ports: 36 | - containerPort: 80 37 | -------------------------------------------------------------------------------- /fixtures/topology/kubernetes-cluster-group-by.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Topology 4 | metadata: 5 | name: kubernetes-clusters 6 | spec: 7 | icon: flux 8 | type: Topology 9 | schedule: "@every 5m" 10 | groupBy: 11 | tag: cluster 12 | components: 13 | - icon: nodes 14 | name: Nodes 15 | components: 16 | - name: Nodes Component 17 | type: lookup 18 | lookup: 19 | catalog: 20 | - name: "" 21 | test: {} 22 | display: 23 | expr: > 24 | dyn(results).map(r, { 25 | 'name': r.name, 26 | 'icon': 'node', 27 | 'status': r.status, 28 | 'status_reason': r.description, 29 | 'selectors': [{'labelSelector': 'app.kubernetes.io/instance='+r.name}], 30 | }).toJSON() 31 | selector: 32 | - types: 33 | - Kubernetes::Node 34 | -------------------------------------------------------------------------------- /checks/postgres.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | 6 | "github.com/flanksource/canary-checker/api/external" 7 | v1 "github.com/flanksource/canary-checker/api/v1" 8 | "github.com/flanksource/canary-checker/pkg" 9 | _ "github.com/lib/pq" // Necessary for postgres 10 | ) 11 | 12 | func init() { 13 | //register metrics here 14 | } 15 | 16 | type PostgresChecker struct{} 17 | 18 | // Type: returns checker type 19 | func (c *PostgresChecker) Type() string { 20 | return "postgres" 21 | } 22 | 23 | // Run: Check every entry from config according to Checker interface 24 | // Returns check result and metrics 25 | func (c *PostgresChecker) Run(ctx *context.Context) pkg.Results { 26 | var results pkg.Results 27 | for _, conf := range ctx.Canary.Spec.Postgres { 28 | results = append(results, c.Check(ctx, conf)...) 29 | } 30 | return results 31 | } 32 | 33 | func (c *PostgresChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 34 | return CheckSQL(ctx, extConfig.(v1.PostgresCheck)) 35 | } 36 | -------------------------------------------------------------------------------- /fixtures/k8s/cronjob_monitor_fail.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: always-failing 6 | namespace: canaries 7 | spec: 8 | schedule: "0 * * * *" 9 | concurrencyPolicy: Forbid 10 | failedJobsHistoryLimit: 1 11 | jobTemplate: 12 | spec: 13 | backoffLimit: 1 14 | template: 15 | spec: 16 | containers: 17 | - name: fail 18 | image: busybox:1.28 19 | imagePullPolicy: IfNotPresent 20 | command: 21 | - /bin/sh 22 | - -c 23 | - exit 1 # always fail 24 | restartPolicy: OnFailure 25 | --- 26 | apiVersion: canaries.flanksource.com/v1 27 | kind: Canary 28 | metadata: 29 | name: monitor-always-failing-job 30 | labels: 31 | "Expected-Fail": "true" 32 | spec: 33 | schedule: "@every 1m" 34 | kubernetes: 35 | - name: "Monitor always-failing job" 36 | kind: CronJob 37 | namespaceSelector: 38 | name: canaries 39 | resource: 40 | name: always-failing 41 | healthy: true 42 | -------------------------------------------------------------------------------- /cmd/offline.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/flanksource/canary-checker/pkg/db" 7 | "github.com/flanksource/commons/logger" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var GoOffline = &cobra.Command{ 12 | Use: "go-offline", 13 | Long: "Download all dependencies.", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | if err := db.GoOffline(); err != nil { 16 | logger.Fatalf("Failed to go offline: %+v", err) 17 | } 18 | 19 | // Run in embedded mode once to download the postgres binary 20 | databaseDir := "temp-database-dir" 21 | if err := os.Mkdir(databaseDir, 0755); err != nil { 22 | logger.Fatalf("Failed to create database directory[%s]: %+v", err) 23 | } 24 | defer os.RemoveAll(databaseDir) 25 | 26 | db.ConnectionString = "embedded://" + databaseDir 27 | if _, err := db.Connect(); err != nil { 28 | logger.Fatalf("Failed to run in embedded mode: %+v", err) 29 | } 30 | if err := db.PostgresServer.Stop(); err != nil { 31 | logger.Fatalf("Failed to stop embedded postgres: %+v", err) 32 | } 33 | 34 | // Intentionally exit with code 0 for Docker 35 | os.Exit(0) 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /fixtures/minimal/metrics-multiple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exchange-rates 5 | spec: 6 | schedule: "every 30 @minutes" 7 | http: 8 | - name: exchange-rates 9 | url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS 10 | metrics: 11 | - name: exchange_rate 12 | type: gauge 13 | value: json.rates.GBP 14 | labels: 15 | - name: "from" 16 | value: "USD" 17 | - name: to 18 | value: GBP 19 | 20 | - name: exchange_rate 21 | type: gauge 22 | value: json.rates.EUR 23 | labels: 24 | - name: "from" 25 | value: "USD" 26 | - name: to 27 | value: EUR 28 | 29 | - name: exchange_rate 30 | type: gauge 31 | value: json.rates.ILS 32 | labels: 33 | - name: "from" 34 | value: "USD" 35 | - name: to 36 | value: ILS 37 | - name: exchange_rate_api 38 | type: histogram 39 | value: elapsed.getMilliseconds() 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/flanksource/canary-checker/cmd" 8 | "github.com/flanksource/canary-checker/pkg/db" 9 | "github.com/flanksource/canary-checker/pkg/runner" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | version = "dev" 15 | commit = "none" 16 | date = "unknown" 17 | ) 18 | 19 | func main() { 20 | if len(commit) > 8 { 21 | version = fmt.Sprintf("%v, commit %v, built at %v", version, commit[0:8], date) 22 | runner.Version = fmt.Sprintf("%v (%v)", version, commit[0:8]) 23 | } else { 24 | runner.Version = version 25 | } 26 | 27 | cmd.Root.AddCommand(&cobra.Command{ 28 | Use: "version", 29 | Short: "Print the version of canary-checker", 30 | Args: cobra.MinimumNArgs(0), 31 | Run: func(cmd *cobra.Command, args []string) { 32 | fmt.Println(version) 33 | }, 34 | }) 35 | 36 | cmd.Root.SetUsageTemplate(cmd.Root.UsageTemplate() + fmt.Sprintf("\nversion: %s\n ", version)) 37 | defer func() { 38 | err := db.StopServer() 39 | if err != nil { 40 | os.Exit(1) 41 | } 42 | }() 43 | if err := cmd.Root.Execute(); err != nil { 44 | os.Exit(1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/aggregate-test/config/server2.yaml: -------------------------------------------------------------------------------- 1 | dns: 2 | - server: 8.8.8.8 3 | port: 53 4 | query: "flanksource.com" 5 | querytype: "A" 6 | minrecords: 1 7 | exactreply: ["8.8.8.8"] 8 | timeout: 10 9 | - server: 8.8.8.8 10 | port: 53 11 | query: "1.2.3.4.nip.io" 12 | querytype: "A" 13 | minrecords: 1 14 | exactreply: ["1.2.3.4"] 15 | timeout: 10 16 | # docker: 17 | # - image: docker.io/library/busybox:1.31.1 18 | # username: 19 | # password: 20 | # expectedDigest: 6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a 21 | # expectedSize: 1219782 22 | # - image: docker.io/library/busybox:random 23 | # username: 24 | # password: 25 | # expectedDigest: abcdef123 26 | # expectedSize: 200 27 | http: 28 | - endpoint: https://httpstat.us/202 29 | thresholdMillis: 3000 30 | responseCodes: [201,200,301] 31 | responseContent: "" 32 | maxSSLExpiry: 7 33 | icmp: 34 | - endpoint: https://github.com 35 | thresholdMillis: 400 36 | packetLossThreshold: 0.5 37 | packetCount: 2 38 | - endpoint: https://google.com 39 | thresholdMillis: 600 40 | packetLossThreshold: 0.01 41 | packetCount: 2 -------------------------------------------------------------------------------- /fixtures/aws/minimal/_setup.sh: -------------------------------------------------------------------------------- 1 | if [[ -z "${AWS_ACCESS_KEY_ID}" ]]; then 2 | printf "\nEnvironment variable for aws access key id (AWS_ACCESS_KEY_ID) is missing!!!\n" 3 | exit 1 4 | else 5 | printf "\nCreating secret from aws access key id ending with '${AWS_ACCESS_KEY_ID:(-8)}'\n" 6 | fi 7 | 8 | if [[ -z "${AWS_SECRET_ACCESS_KEY}" ]]; then 9 | printf "\nEnvironment variable for aws secret access key (AWS_SECRET_ACCESS_KEY) is missing!!!\n" 10 | exit 1 11 | else 12 | printf "\nCreating secret from aws secret access key ending with '${AWS_SECRET_ACCESS_KEY:(-8)}'\n" 13 | fi 14 | 15 | if [[ -z "${AWS_SESSION_TOKEN}" ]]; then 16 | printf "\nEnvironment variable for aws session token (AWS_SESSION_TOKEN) is missing!!!\n" 17 | exit 1 18 | else 19 | printf "\nCreating secret from aws session token ending with '${AWS_SESSION_TOKEN:(-4)}'\n" 20 | fi 21 | 22 | kubectl create secret generic aws-credentials \ 23 | --from-literal=AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" \ 24 | --from-literal=AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" \ 25 | --from-literal=AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN}" \ 26 | --namespace default 27 | -------------------------------------------------------------------------------- /pkg/topology/suite_test.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | "testing" 5 | 6 | dutyContext "github.com/flanksource/duty/context" 7 | "github.com/flanksource/duty/job" 8 | "github.com/flanksource/duty/models" 9 | "github.com/flanksource/duty/query" 10 | "github.com/flanksource/duty/tests/setup" 11 | "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var ( 16 | DefaultContext dutyContext.Context 17 | ) 18 | 19 | func cleanupQueryCache() { 20 | Expect(query.FlushComponentCache(DefaultContext)).To(BeNil()) 21 | Expect(query.FlushConfigCache(DefaultContext)).To(BeNil()) 22 | query.FlushGettersCache() 23 | } 24 | 25 | func expectJobToPass(j *job.Job) { 26 | history, err := j.FindHistory() 27 | Expect(err).To(BeNil()) 28 | Expect(len(history)).To(BeNumerically(">=", 1)) 29 | Expect(history[0].Status).To(BeElementOf(models.StatusSuccess, models.StatusFinished)) 30 | } 31 | 32 | func TestTopologyJobs(t *testing.T) { 33 | RegisterFailHandler(ginkgo.Fail) 34 | ginkgo.RunSpecs(t, "Topology") 35 | } 36 | 37 | var _ = ginkgo.BeforeSuite(func() { 38 | DefaultContext = setup.BeforeSuiteFn().WithTrace() 39 | 40 | }) 41 | var _ = ginkgo.AfterSuite(setup.AfterSuiteFn) 42 | -------------------------------------------------------------------------------- /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | permissions: 4 | contents: read 5 | checks: write 6 | issues: write 7 | pull-requests: write 8 | name: Go Test 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 15 | with: 16 | go-version: 1.22.x 17 | - name: Checkout code 18 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 19 | - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 20 | with: 21 | path: | 22 | ~/go/pkg/mod 23 | ~/.cache/go-build 24 | key: cache-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | cache- 27 | - name: Test 28 | run: make test 29 | - name: Publish Unit Test Results 30 | uses: EnricoMi/publish-unit-test-result-action@f355d34d53ad4e7f506f699478db2dd71da9de5f # v2.15.1 31 | if: always() && github.event.repository.fork == 'false' 32 | with: 33 | files: test/test-results.xml 34 | check_name: E2E - ${{matrix.suite}} 35 | -------------------------------------------------------------------------------- /checks/mssql.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | 6 | "github.com/flanksource/canary-checker/api/external" 7 | v1 "github.com/flanksource/canary-checker/api/v1" 8 | "github.com/flanksource/canary-checker/pkg" 9 | _ "github.com/microsoft/go-mssqldb" // required by mssql 10 | ) 11 | 12 | func init() { 13 | //register metrics here 14 | } 15 | 16 | type MssqlChecker struct{} 17 | 18 | // Type: returns checker type 19 | func (c *MssqlChecker) Type() string { 20 | return "mssql" 21 | } 22 | 23 | // Run - Check every entry from config according to Checker interface 24 | // Returns check result and metrics 25 | func (c *MssqlChecker) Run(ctx *context.Context) pkg.Results { 26 | var results pkg.Results 27 | for _, conf := range ctx.Canary.Spec.Mssql { 28 | results = append(results, c.Check(ctx, conf)...) 29 | } 30 | return results 31 | } 32 | 33 | // Check CheckConfig : Attempts to connect to a DB using the specified 34 | // 35 | // driver and connection string 36 | // 37 | // Returns check result and metrics 38 | func (c *MssqlChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 39 | return CheckSQL(ctx, extConfig.(v1.MssqlCheck)) 40 | } 41 | -------------------------------------------------------------------------------- /checks/stubs_windows.go: -------------------------------------------------------------------------------- 1 | //+go:build windows 2 | //+go:build !linux !darwin 3 | 4 | package checks 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/flanksource/canary-checker/api/context" 10 | "github.com/flanksource/canary-checker/api/external" 11 | "github.com/flanksource/canary-checker/pkg" 12 | ) 13 | 14 | // FIXME: disabling due to the following error 15 | // Error: ../../../go/pkg/mod/github.com/containerd/containerd@v1.4.0/archive/tar_windows.go:234:3: cannot use syscall.NsecToFiletime(hdr.AccessTime.UnixNano()) (type syscall.Filetime) as type "golang.org/x/sys/windows".Filetime in field value 16 | type ContainerdPullChecker struct{} 17 | 18 | func (c *ContainerdPullChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 19 | result := pkg.Fail(extConfig, ctx.Canary).ErrorMessage(errors.New("containerd not supported on windows")) 20 | var results pkg.Results 21 | results = append(results, result) 22 | return results 23 | } 24 | 25 | func (c *ContainerdPullChecker) Type() string { 26 | return "containerdPull" 27 | } 28 | 29 | func (c *ContainerdPullChecker) Run(ctx *context.Context) pkg.Results { 30 | return pkg.SetupError(ctx.Canary, errors.New("containerd not supported on windows")) 31 | } 32 | -------------------------------------------------------------------------------- /fixtures/k8s/_setup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: secrets 5 | namespace: canaries 6 | stringData: 7 | DOCKER_USERNAME: test 8 | DOCKER_PASSWORD: password 9 | --- 10 | apiVersion: v1 11 | metadata: 12 | name: basic-auth 13 | namespace: canaries 14 | kind: ConfigMap 15 | data: 16 | pass: world 17 | user: hello 18 | --- 19 | apiVersion: v1 20 | metadata: 21 | name: basic-auth 22 | namespace: canaries 23 | kind: Secret 24 | stringData: 25 | pass: world 26 | user: hello 27 | --- 28 | apiVersion: v1 29 | kind: Pod 30 | metadata: 31 | name: k8s-check-ready 32 | namespace: canaries 33 | labels: 34 | app: k8s-ready 35 | spec: 36 | containers: 37 | - name: hello 38 | image: public.ecr.aws/docker/library/busybox:1.35.0 39 | command: ["sh", "-c", 'echo "Hello, Kubernetes!" && sleep 3600'] 40 | restartPolicy: OnFailure 41 | --- 42 | apiVersion: v1 43 | kind: Pod 44 | metadata: 45 | name: k8s-check-not-ready 46 | namespace: canaries 47 | labels: 48 | app: k8s-not-ready 49 | spec: 50 | containers: 51 | - name: hello 52 | image: busybox-random 53 | command: ["sh", "-c", 'echo "Hello, Kubernetes!" && sleep 3600'] 54 | restartPolicy: OnFailure 55 | -------------------------------------------------------------------------------- /fixtures/datasources/alertmanager_mix.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: alertmanager 5 | spec: 6 | interval: 30 7 | alertmanager: 8 | - url: https://alertmanager.demo.aws.flanksource.com 9 | name: alertmanager-check 10 | alerts: 11 | - .* 12 | ignore: 13 | - KubeScheduler.* 14 | exclude_filters: 15 | namespace: elastic-system 16 | transform: 17 | expr: | 18 | results.alerts.map(r, { 19 | 'name': r.name + r.fingerprint, 20 | 'namespace': 'namespace' in r.labels ? r.labels.namespace : '', 21 | 'labels': r.labels, 22 | 'icon': 'alert', 23 | 'message': r.message, 24 | 'description': r.message, 25 | }).toJSON() 26 | relationships: 27 | components: 28 | - name: 29 | label: pod 30 | namespace: 31 | label: namespace 32 | type: 33 | value: KubernetesPod 34 | configs: 35 | - name: 36 | label: pod 37 | namespace: 38 | label: namespace 39 | type: 40 | value: Kubernetes::Pod 41 | -------------------------------------------------------------------------------- /pkg/db/postgrest.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/flanksource/commons/deps" 7 | "github.com/flanksource/commons/logger" 8 | ) 9 | 10 | var PostgRESTVersion = "v9.0.0" 11 | var PostgRESTServerPort = 3000 12 | 13 | func PostgRESTEndpoint() string { 14 | return "http://localhost:" + strconv.Itoa(PostgRESTServerPort) 15 | } 16 | 17 | func GoOffline() error { 18 | return getBinary()("--help") 19 | } 20 | 21 | func getBinary() deps.BinaryFunc { 22 | return deps.BinaryWithEnv("postgREST", PostgRESTVersion, ".bin", map[string]string{ 23 | "PGRST_DB_URI": ConnectionString, 24 | "PGRST_DB_PORT": strconv.Itoa(PostgRESTServerPort), 25 | "PGRST_DB_SCHEMA": "public", // Database is set with default schema. Which is public. See /pkg/db/migrations/ 26 | "PGRST_DB_ANON_ROLE": "postgrest_api", // See: pkg/db/migrations/4_roles.sql 27 | "PGRST_OPENAPI_SERVER_PROXY_URI": HTTPEndpoint, 28 | "PGRST_LOG_LEVEL": "info", // https://postgrest.org/en/stable/configuration.html?highlight=log_level#log-level 29 | }) 30 | } 31 | 32 | func StartPostgrest() { 33 | if err := getBinary()(""); err != nil { 34 | logger.Errorf("Failed to start postgREST: %v", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover any security vulnerabilities within this project, please report them to our team immediately. We appreciate your help in making this project more secure for everyone. 6 | 7 | To report a vulnerability, please follow these steps: 8 | 9 | 1. **Email**: Send an email to our security team at [security@flanksource.com](mailto:security@flanksource.com) with a detailed description of the vulnerability. 10 | 2. **Subject Line**: Use the subject line "Security Vulnerability Report" to ensure prompt attention. 11 | 3. **Information**: Provide as much information as possible about the vulnerability, including steps to reproduce it and any supporting documentation or code snippets. 12 | 4. **Confidentiality**: We prioritize the confidentiality of vulnerability reports. Please avoid publicly disclosing the issue until we have had an opportunity to address it. 13 | 14 | Our team will respond to your report as soon as possible and work towards a solution. We appreciate your responsible disclosure and cooperation in maintaining the security of this project. 15 | 16 | Thank you for your contribution to the security of this project! 17 | 18 | **Note:** This project follows responsible disclosure practices. 19 | -------------------------------------------------------------------------------- /pkg/api/topology.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/flanksource/canary-checker/pkg/topology" 7 | "github.com/flanksource/duty/context" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | // TopologyQuery godoc 12 | // @Id TopologyQuery 13 | // @Summary Topology query 14 | // @Description Query the topology graph 15 | // @Tags topology 16 | // @Produce json 17 | // @Param id query string false "Topology ID" 18 | // @Param topologyId query string false "Topology ID" 19 | // @Param componentId query string false "Component ID" 20 | // @Param owner query string false "Owner" 21 | // @Param status query string false "Comma separated list of status" 22 | // @Param types query string false "Comma separated list of types" 23 | // @Param flatten query string false "Flatten the topology" 24 | // @Success 200 {object} pkg.Components 25 | // @Router /api/topology [get] 26 | func Topology(c echo.Context) error { 27 | ctx := c.Request().Context().(context.Context) 28 | params := topology.NewTopologyParams(c.QueryParams()) 29 | results, err := topology.Query(ctx, params) 30 | if err != nil { 31 | return errorResponse(c, err, http.StatusBadRequest) 32 | } 33 | 34 | return c.JSON(http.StatusOK, results) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/output/junit.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/flanksource/canary-checker/pkg" 7 | "github.com/flanksource/commons/console" 8 | "github.com/flanksource/commons/logger" 9 | ) 10 | 11 | func GetJunitReport(results []*pkg.CheckResult) string { 12 | var testCases []console.JUnitTestCase 13 | var failed int 14 | var totalTime int64 15 | for _, result := range results { 16 | totalTime += result.Duration 17 | testCase := console.JUnitTestCase{ 18 | Classname: result.Check.GetType(), 19 | Name: result.Check.GetDescription(), 20 | Time: strconv.Itoa(int(result.Duration)), 21 | } 22 | if !result.Pass { 23 | failed++ 24 | testCase.Failure = &console.JUnitFailure{ 25 | Message: result.Message, 26 | } 27 | } 28 | testCases = append(testCases, testCase) 29 | } 30 | testSuite := console.JUnitTestSuite{ 31 | Tests: len(results), 32 | Failures: failed, 33 | Time: strconv.Itoa(int(totalTime)), 34 | Name: "canary-checker-run", 35 | TestCases: testCases, 36 | } 37 | testSuites := console.JUnitTestSuites{ 38 | Suites: []console.JUnitTestSuite{ 39 | testSuite, 40 | }, 41 | } 42 | report, err := testSuites.ToXML() 43 | if err != nil { 44 | logger.Fatalf("error creating junit results: %v", err) 45 | } 46 | return report 47 | } 48 | -------------------------------------------------------------------------------- /fixtures/k8s/junit_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: junit-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: '@every 2h' 9 | owner: DBAdmin 10 | severity: high 11 | junit: 12 | - testResults: "/tmp/junit-results/" 13 | name: junit-fail 14 | display: 15 | template: | 16 | ✅ {{.results.passed}} ❌ {{.results.failed}} in 🕑 {{.results.duration}} 17 | {{- range $r := .results.suites}} 18 | {{- if gt (conv.ToInt $r.failed) 0 }} 19 | {{$r.name}} ✅ {{$r.passed}} ❌ {{$r.failed}} in 🕑 {{$r.duration}} 20 | {{- range $t := $r.tests }} 21 | {{- if not (eq $t.status "passed")}} 22 | ❌ {{$t.classname}}/{{$t.name}} in 🕑 {{$t.duration}} 23 | {{- if $t.message}} 24 | {{ $t.message }} 25 | {{- end }} 26 | {{- if $t.stdout}} 27 | {{$t.stdout}} 28 | {{- end }} 29 | {{- if $t.sterr}} 30 | {{$t.stderr}} 31 | {{- end }} 32 | {{- end }} 33 | {{- end }} 34 | {{- end }} 35 | {{- end }} 36 | spec: 37 | containers: 38 | - name: jes 39 | image: docker.io/tarun18/junit-test-fail 40 | command: ["/start.sh"] 41 | -------------------------------------------------------------------------------- /pkg/api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func Test_getMostSuitableWindowDuration(t *testing.T) { 9 | day := time.Hour * 24 10 | 11 | tests := []struct { 12 | schedule time.Duration // how often the check is run 13 | rangeDuration time.Duration 14 | expected time.Duration // the best duration to partition the range 15 | }{ 16 | {time.Second * 30, time.Minute * 5, 0}, 17 | {time.Second * 30, time.Minute * 30, 0}, 18 | {time.Second * 30, time.Hour * 2, time.Minute}, 19 | {time.Second * 30, time.Hour * 12, time.Minute * 5}, 20 | {time.Second * 30, day * 2, time.Minute * 30}, 21 | {time.Hour, day * 4, 0}, 22 | {time.Hour, day * 5, 0}, 23 | {time.Hour, day * 6, 0}, 24 | {time.Hour, day * 12, time.Hour * 3}, 25 | {time.Second * 30, day * 8, time.Hour * 3}, 26 | {time.Second * 30, day * 30, time.Hour * 6}, 27 | {time.Second * 30, day * 90, day}, 28 | {time.Second * 30, day * 365, day * 7}, 29 | } 30 | 31 | for _, td := range tests { 32 | t.Run(td.rangeDuration.String(), func(t *testing.T) { 33 | totalChecks := int(td.rangeDuration / td.schedule) 34 | result := GetBestPartitioner(totalChecks, td.rangeDuration) 35 | if result != td.expected { 36 | t.Errorf("expected %v, but got %v", td.expected, result) 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fixtures/k8s/junit_pass_metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: junit-metrics 5 | labels: 6 | part-of: canary-tools 7 | spec: 8 | schedule: "@every 2h" 9 | severity: high 10 | junit: 11 | - testResults: "/tmp/junit-results/" 12 | name: junit-pass 13 | test: 14 | expr: results.failed == 0 && results.passed > 0 15 | display: 16 | expr: "string(results.failed) + ' of ' + string(results.passed)" 17 | spec: 18 | containers: 19 | - name: jes 20 | image: docker.io/tarun18/junit-test-pass 21 | command: ["/start.sh"] 22 | metrics: 23 | - name: junit_check_pass_count 24 | type: gauge 25 | value: results.passed 26 | labels: 27 | - name: suite_name 28 | valueExpr: results.suites[0].name 29 | - name: junit_check_failed_count 30 | type: gauge 31 | value: results.failed 32 | labels: 33 | - name: part_of 34 | valueExpr: canary.labels['part-of'] 35 | - name: junit_check_duration_ms 36 | type: histogram 37 | value: results.duration * 1000.0 38 | labels: 39 | - name: suite_name 40 | valueExpr: results.suites[0].name 41 | -------------------------------------------------------------------------------- /chart/templates/postgres-statefulset.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.db.external.create true }} 2 | apiVersion: apps/v1 3 | kind: StatefulSet 4 | metadata: 5 | name: postgresql 6 | labels: 7 | {{- include "postgresql.labels" . | nindent 4 }} 8 | spec: 9 | serviceName: postgresql 10 | selector: 11 | matchLabels: 12 | app: postgresql 13 | {{- include "postgresql.selectorLabels" . | nindent 6 }} 14 | replicas: 1 15 | template: 16 | metadata: 17 | labels: 18 | app: postgresql 19 | {{- include "postgresql.selectorLabels" . | nindent 8 }} 20 | spec: 21 | containers: 22 | - name: postgresql 23 | image: {{ include "canary-checker.postgres.imageString" . }} 24 | volumeMounts: 25 | - name: postgresql 26 | mountPath: /data 27 | envFrom: 28 | - secretRef: 29 | name: {{ .Values.db.external.secretKeyRef.name }} 30 | volumeClaimTemplates: 31 | - metadata: 32 | name: postgresql 33 | spec: 34 | accessModes: ["ReadWriteOnce"] 35 | {{- if not (eq .Values.db.external.storageClass "") }} 36 | storageClassName: {{ .Values.db.external.storageClass }} 37 | {{- end }} 38 | resources: 39 | requests: 40 | storage: {{ .Values.db.external.storage }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: kubernetes-bundle 5 | spec: 6 | interval: 60 7 | kubernetes: 8 | - kind: Node 9 | ready: true 10 | name: node-bundle 11 | transform: 12 | expr: | 13 | dyn(results).map(r, { 14 | 'name': r.Object.metadata.name, 15 | 'namespace': r.Object.metadata.?namespace.orValue(null), 16 | 'labels': r.Object.metadata.labels, 17 | 'pass': k8s.isHealthy(r.Object), 18 | 'message': k8s.getHealth(r.Object).message, 19 | 'error': k8s.getHealth(r.Object).message, 20 | }).toJSON() 21 | - kind: Pod 22 | ready: true 23 | name: pod-bundle 24 | resource: 25 | labelSelector: app != k8s-not-ready, app != k8s-ready, Expected-Fail != true, canary-checker.flanksource.com/generated != true, !canary-checker.flanksource.com/check 26 | transform: 27 | expr: | 28 | dyn(results).map(r, { 29 | 'name': r.Object.metadata.name, 30 | 'namespace': r.Object.metadata.namespace, 31 | 'labels': r.Object.metadata.labels, 32 | 'pass': k8s.isHealthy(r.Object), 33 | 'message': k8s.getHealth(r.Object).message, 34 | 'error': k8s.getHealth(r.Object).message, 35 | }).toJSON() 36 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the canaries v1 API group 18 | // +kubebuilder:object:generate:=true 19 | // +groupName=canaries.flanksource.com 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "canaries.flanksource.com", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | # Branches are defined in the github action workflow 2 | # We create pre-releases on automated push to main and 3 | # a release is created manually by triggering the workflow 4 | branches: [] 5 | plugins: 6 | - - "@semantic-release/commit-analyzer" 7 | - releaseRules: 8 | - { type: doc, scope: README, release: patch } 9 | - { type: fix, release: patch } 10 | - { type: chore, release: patch } 11 | - { type: refactor, release: patch } 12 | - { type: feat, release: patch } 13 | - { type: ci, release: false } 14 | - { type: style, release: false } 15 | - { type: major, release: major } 16 | parserOpts: 17 | noteKeywords: 18 | - MAJOR RELEASE 19 | - "@semantic-release/release-notes-generator" 20 | - - "@semantic-release/github" 21 | - assets: 22 | - path: ./.bin/canary-checker-amd64 23 | name: canary-checker-amd64 24 | - path: ./.bin/canary-checker.exe 25 | name: canary-checker.exe 26 | - path: ./.bin/canary-checker_osx-amd64 27 | name: canary-checker_osx-amd64 28 | - path: ./.bin/canary-checker_osx-arm64 29 | name: canary-checker_osx-arm64 30 | - path: ./.bin/release.yaml 31 | name: release.yaml 32 | # From: https://github.com/semantic-release/github/pull/487#issuecomment-1486298997 33 | successComment: false 34 | failTitle: false 35 | -------------------------------------------------------------------------------- /api/external/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | /* 4 | Copyright 2020 The Kubernetes authors. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Code generated by controller-gen. DO NOT EDIT. 20 | 21 | package external 22 | 23 | import () 24 | 25 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 26 | func (in *Metrics) DeepCopyInto(out *Metrics) { 27 | *out = *in 28 | if in.Labels != nil { 29 | in, out := &in.Labels, &out.Labels 30 | *out = make(MetricLabels, len(*in)) 31 | copy(*out, *in) 32 | } 33 | } 34 | 35 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metrics. 36 | func (in *Metrics) DeepCopy() *Metrics { 37 | if in == nil { 38 | return nil 39 | } 40 | out := new(Metrics) 41 | in.DeepCopyInto(out) 42 | return out 43 | } 44 | -------------------------------------------------------------------------------- /checks/catalog.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | canaryContext "github.com/flanksource/canary-checker/api/context" 5 | v1 "github.com/flanksource/canary-checker/api/v1" 6 | "github.com/flanksource/canary-checker/pkg" 7 | "github.com/flanksource/duty/query" 8 | ) 9 | 10 | type CatalogChecker struct{} 11 | 12 | func (c *CatalogChecker) Type() string { 13 | return "catalog" 14 | } 15 | 16 | func (c *CatalogChecker) Run(ctx *canaryContext.Context) pkg.Results { 17 | var results pkg.Results 18 | for _, conf := range ctx.Canary.Spec.Catalog { 19 | results = append(results, c.Check(ctx, conf)...) 20 | } 21 | 22 | return results 23 | } 24 | 25 | func (c *CatalogChecker) Check(ctx *canaryContext.Context, check v1.CatalogCheck) pkg.Results { 26 | result := pkg.Success(check, ctx.Canary) 27 | 28 | var results pkg.Results 29 | results = append(results, result) 30 | 31 | items, err := query.FindConfigsByResourceSelector(ctx.Context, check.Selector...) 32 | if err != nil { 33 | return results.Failf("failed to fetch catalogs: %v", err) 34 | } 35 | 36 | var configItems []map[string]any 37 | for _, item := range items { 38 | ci := item.AsMap() 39 | // The config should be map[string]any so 40 | // that it can be accessed directly in templating 41 | ci["config"], _ = item.ConfigJSONStringMap() 42 | configItems = append(configItems, ci) 43 | } 44 | result.AddDetails(configItems) 45 | return results 46 | } 47 | -------------------------------------------------------------------------------- /checks/checker.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | "github.com/flanksource/canary-checker/api/external" 6 | "github.com/flanksource/canary-checker/pkg" 7 | ) 8 | 9 | type Checks []external.Check 10 | 11 | func (c Checks) Includes(checker Checker) bool { 12 | for _, check := range c { 13 | if check.GetType() == checker.Type() { 14 | return true 15 | } 16 | } 17 | return false 18 | } 19 | 20 | type Checker interface { 21 | Run(ctx *context.Context) pkg.Results 22 | Type() string 23 | } 24 | 25 | var All = []Checker{ 26 | &AlertManagerChecker{}, 27 | &AwsConfigChecker{}, 28 | &AwsConfigRuleChecker{}, 29 | &AzureDevopsChecker{}, 30 | &CloudWatchChecker{}, 31 | &CatalogChecker{}, 32 | &DatabaseBackupChecker{}, 33 | &DNSChecker{}, 34 | &DynatraceChecker{}, 35 | &ElasticsearchChecker{}, 36 | &ExecChecker{}, 37 | &FolderChecker{}, 38 | &GitHubChecker{}, 39 | &GitProtocolChecker{}, 40 | &HTTPChecker{}, 41 | &IcmpChecker{}, 42 | &JmeterChecker{}, 43 | &JunitChecker{}, 44 | &KubernetesChecker{}, 45 | &KubernetesResourceChecker{}, 46 | &LdapChecker{}, 47 | &MongoDBChecker{}, 48 | &MssqlChecker{}, 49 | &MysqlChecker{}, 50 | &OpenSearchChecker{}, 51 | &PostgresChecker{}, 52 | &PrometheusChecker{}, 53 | &RedisChecker{}, 54 | &ResticChecker{}, 55 | &S3Checker{}, 56 | NewNamespaceChecker(), 57 | NewPodChecker(), 58 | NewTCPChecker(), 59 | } 60 | -------------------------------------------------------------------------------- /cmd/output/csv.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/flanksource/canary-checker/pkg" 7 | "github.com/jszwec/csvutil" 8 | ) 9 | 10 | type CSVResult struct { 11 | Name string `csv:"name"` 12 | Namespace string `csv:"namespace"` 13 | Endpoint string `csv:"endpoint"` 14 | CheckType string `csv:"checkType"` 15 | Pass bool `csv:"pass"` 16 | Duration string `csv:"duration"` 17 | Description string `csv:"description,omitempty"` 18 | Message string `csv:"message,omitempty"` 19 | Error string `csv:"error,omitempty"` 20 | Invalid bool `csv:"invalid,omitempty"` 21 | } 22 | 23 | func GetCSVReport(checkResults []*pkg.CheckResult) (string, error) { 24 | var results []CSVResult 25 | for _, checkResult := range checkResults { 26 | result := CSVResult{ 27 | Name: checkResult.Canary.Name, 28 | Namespace: checkResult.Canary.Namespace, 29 | Endpoint: checkResult.Check.GetEndpoint(), 30 | CheckType: checkResult.Check.GetType(), 31 | Pass: checkResult.Pass, 32 | Invalid: checkResult.Invalid, 33 | Duration: strconv.Itoa(int(checkResult.Duration)), 34 | Description: checkResult.Check.GetDescription(), 35 | Message: checkResult.Message, 36 | Error: checkResult.Error, 37 | } 38 | results = append(results, result) 39 | } 40 | 41 | csv, err := csvutil.Marshal(results) 42 | return string(csv), err 43 | } 44 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_service_fail.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: invalid-service-spec-test 6 | namespace: default 7 | labels: 8 | "Expected-Fail": "true" 9 | spec: 10 | schedule: "@every 5m" 11 | kubernetesResource: 12 | - name: invalid service configuration 13 | namespace: default 14 | description: "deploy httpbin & check that it's accessible via service" 15 | waitFor: 16 | interval: 5s 17 | timeout: 30s 18 | resources: 19 | - apiVersion: v1 20 | kind: Pod 21 | metadata: 22 | name: httpbin 23 | namespace: default 24 | labels: 25 | app: httpbin-faulty 26 | spec: 27 | containers: 28 | - name: httpbin 29 | image: "kennethreitz/httpbin:latest" 30 | ports: 31 | - containerPort: 80 32 | - apiVersion: v1 33 | kind: Service 34 | metadata: 35 | name: httpbin-faulty-svc 36 | namespace: default 37 | spec: 38 | selector: 39 | app: httpbin-faulty 40 | ports: 41 | - port: 8080 42 | targetPort: 8080 43 | checks: 44 | - http: 45 | - name: Call httpbin service 46 | url: "http://httpbin-faulty-svc.default.svc" 47 | -------------------------------------------------------------------------------- /chart/templates/postgres-secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if eq .Values.db.external.create true }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ .Values.db.external.secretKeyRef.name }} 6 | labels: 7 | {{- include "canary-checker.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/resource-policy": "keep" 10 | type: Opaque 11 | stringData: 12 | {{- $secretInj := ( lookup "v1" "Secret" .Release.Namespace "postgres-connection" ).data }} 13 | {{- $secretObj := ( lookup "v1" "Secret" .Release.Namespace .Values.db.external.secretKeyRef.name ).data }} 14 | {{- $user := (( get $secretInj "POSTGRES_USER" ) | b64dec ) | default (( get $secretObj "POSTGRES_USER" ) | b64dec ) | default "postgres" }} 15 | {{- $password := (( get $secretInj "POSTGRES_PASSWORD" ) | b64dec ) | default (( get $secretObj "POSTGRES_PASSWORD" ) | b64dec ) | default ( randAlphaNum 32 ) }} 16 | {{- $host := print "postgres." .Release.Namespace ".svc.cluster.local" }} 17 | {{- $url := print "postgresql://" $user ":" $password "@" $host }} 18 | {{- $canaryCheckerUrl := ( get $secretObj .Values.db.external.secretKeyRef.key ) | default ( print $url "/canarychecker?sslmode=disable" ) }} 19 | POSTGRES_USER: {{ $user | quote }} 20 | POSTGRES_PASSWORD: {{ $password | quote }} 21 | POSTGRES_HOST: {{ $host | quote }} 22 | POSTGRES_PORT: "5432" 23 | POSTGRES_DB: "canarychecker" 24 | {{ .Values.db.external.secretKeyRef.key }}: {{ $canaryCheckerUrl | quote }} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /fixtures/minimal/http_pass_single.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-pass-single 5 | labels: 6 | canary: http 7 | spec: 8 | interval: 30 9 | http: 10 | - endpoint: https://httpbin.demo.aws.flanksource.com/status/200 11 | name: http-deprecated-endpoint 12 | - name: http-minimal-check 13 | url: https://httpbin.demo.aws.flanksource.com/status/200 14 | metrics: 15 | - name: httpbin_2xx_count 16 | type: counter 17 | value: "code == 200 ? 1 : 0" 18 | labels: 19 | - name: name 20 | value: httpbin_2xx_count 21 | - name: check_name 22 | valueExpr: check.name 23 | - name: status_class 24 | valueExpr: string(code).charAt(0) 25 | - name: http-param-tests 26 | url: https://httpbin.demo.aws.flanksource.com/status/200 27 | responseCodes: [201, 200, 301] 28 | responseContent: "" 29 | maxSSLExpiry: 7 30 | - name: http-expr-tests 31 | url: https://httpbin.demo.aws.flanksource.com/status/200 32 | test: 33 | expr: "code in [200,201,301] && sslAge > Duration('7d')" 34 | display: 35 | template: "code={{.code}}, age={{.sslAge}}" 36 | 37 | - name: http-headers 38 | url: https://httpbin.demo.aws.flanksource.com/headers 39 | test: 40 | expr: json.headers["User-Agent"].startsWith("canary-checker/") 41 | -------------------------------------------------------------------------------- /hack/generate-schemas/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | v1 "github.com/flanksource/canary-checker/api/v1" 9 | "github.com/flanksource/commons/logger" 10 | "github.com/flanksource/duty/schema/openapi" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var schemas = map[string]interface{}{ 15 | "canary": &v1.Canary{}, 16 | "topology": &v1.Topology{}, 17 | "component": &v1.Component{}, 18 | } 19 | 20 | var generateSchema = &cobra.Command{ 21 | Use: "generate-schema", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | for file, obj := range schemas { 24 | p := path.Join(schemaPath, file+".schema.json") 25 | if err := openapi.WriteSchemaToFile(p, obj); err != nil { 26 | logger.Fatalf("unable to save schema: %v", err) 27 | } 28 | logger.Infof("Saved OpenAPI schema to %s", p) 29 | } 30 | 31 | for _, check := range v1.AllChecks { 32 | p := path.Join(schemaPath, fmt.Sprintf("health_%s.schema.json", check.GetType())) 33 | if err := openapi.WriteSchemaToFile(p, check); err != nil { 34 | logger.Fatalf("unable to save schema: %v", err) 35 | } 36 | 37 | logger.Infof("Saved OpenAPI schema to %s", p) 38 | } 39 | }, 40 | } 41 | 42 | var schemaPath string 43 | 44 | func main() { 45 | generateSchema.Flags().StringVar(&schemaPath, "schema-path", "../../config/schemas", "Path to save JSON schema to") 46 | if err := generateSchema.Execute(); err != nil { 47 | os.Exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/labels/labels.go: -------------------------------------------------------------------------------- 1 | package labels 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | 8 | "github.com/flanksource/commons/logger" 9 | ) 10 | 11 | var IgnoreLabels = []string{ 12 | "pod-template-hash", 13 | "kustomize.toolkit.fluxcd.io", 14 | } 15 | 16 | func FilterLabels(labels map[string]string) map[string]string { 17 | var new = make(map[string]string) 18 | outer: 19 | for k, v := range labels { 20 | for _, ignore := range IgnoreLabels { 21 | if strings.HasPrefix(k, ignore) { 22 | continue outer 23 | } 24 | } 25 | new[k] = v 26 | } 27 | return new 28 | } 29 | 30 | func LoadFromFile(path string) map[string]string { 31 | result := make(map[string]string) 32 | if _, err := os.Stat(path); os.IsNotExist(err) { 33 | // No label metadata mounted into the operator pod 34 | logger.Infof("No label file mounted at %s", path) 35 | return result 36 | } 37 | f, err := os.Open(path) 38 | if err != nil { 39 | logger.Errorf("Failed to read label file (%s): %v", path, err) 40 | return result 41 | } 42 | defer func() { 43 | if err = f.Close(); err != nil { 44 | logger.Errorf("Failed to close label file (%s): %v", path, err) 45 | } 46 | }() 47 | 48 | s := bufio.NewScanner(f) 49 | for s.Scan() { 50 | line := strings.Split(s.Text(), "=") 51 | result[line[0]] = line[1] 52 | } 53 | err = s.Err() 54 | if err != nil { 55 | logger.Errorf("Failed to read label file (%s): %v", path, err) 56 | } 57 | 58 | return result 59 | } 60 | -------------------------------------------------------------------------------- /test/aggregate-test/config/main.yaml: -------------------------------------------------------------------------------- 1 | dns: 2 | - server: 8.8.8.8 3 | port: 53 4 | query: "flanksource.com" 5 | querytype: "A" 6 | minrecords: 1 7 | exactreply: ["8.8.8.8"] 8 | timeout: 10 9 | - server: 8.8.8.8 10 | port: 53 11 | query: "1.2.3.4.nip.io" 12 | querytype: "A" 13 | minrecords: 1 14 | exactreply: ["1.2.3.4"] 15 | timeout: 10 16 | # docker: 17 | # - image: docker.io/library/busybox:1.31.1 18 | # username: 19 | # password: 20 | # expectedDigest: 6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a 21 | # expectedSize: 1219782 22 | # - image: docker.io/library/busybox:random 23 | # username: 24 | # password: 25 | # expectedDigest: abcdef123 26 | # expectedSize: 200 27 | http: 28 | - endpoint: https://httpstat.us/200 29 | thresholdMillis: 3000 30 | responseCodes: [201,200,301] 31 | responseContent: "" 32 | maxSSLExpiry: 7 33 | - endpoint: https://ttpstat.us/500 34 | thresholdMillis: 3000 35 | responseCodes: [201,200,301] 36 | responseContent: "" 37 | maxSSLExpiry: 60 38 | endpoint: https://httpstat.us/500 39 | thresholdMillis: 3000 40 | responseCodes: [201,200,301] 41 | responseContent: "" 42 | maxSSLExpiry: 60 43 | icmp: 44 | - endpoint: https://github.com 45 | thresholdMillis: 400 46 | packetLossThreshold: 0.5 47 | packetCount: 2 48 | - endpoint: https://google.com 49 | thresholdMillis: 600 50 | packetLossThreshold: 0.01 51 | packetCount: 2 -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_pod_exit_code_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-exit-code-check 6 | namespace: default 7 | labels: 8 | "Expected-Fail": "false" 9 | spec: 10 | schedule: "@every 5m" 11 | kubernetesResource: 12 | - name: "pod exit code" 13 | description: "Create pod & check its exit code" 14 | namespace: default 15 | resources: 16 | - apiVersion: v1 17 | kind: Pod 18 | metadata: 19 | name: "hello-world-{{strings.ToLower (random.Alpha 10)}}" 20 | namespace: default 21 | spec: 22 | restartPolicy: Never 23 | containers: 24 | - name: hello-world 25 | image: hello-world 26 | waitFor: 27 | expr: "dyn(resources).all(r, k8s.isHealthy(r))" 28 | interval: "1s" 29 | timeout: "20s" 30 | checkRetries: 31 | delay: 2s 32 | timeout: 5m 33 | checks: 34 | - kubernetes: 35 | - name: exit-code-check 36 | kind: Pod 37 | namespaceSelector: 38 | name: default 39 | resource: 40 | name: "{{(index .resources 0).Object.metadata.name}}" 41 | test: 42 | expr: > 43 | size(results) == 1 && 44 | results[0].Object.status.containerStatuses[0].state.terminated.exitCode == 0 45 | -------------------------------------------------------------------------------- /pkg/api/http.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/flanksource/commons/logger" 7 | "github.com/labstack/echo/v4" 8 | ) 9 | 10 | type HTTPError struct { 11 | Error string `json:"error"` 12 | Message string `json:"message,omitempty"` 13 | } 14 | 15 | type HTTPSuccess struct { 16 | Message string `json:"message"` 17 | Payload any `json:"payload,omitempty"` 18 | } 19 | 20 | // WriteError writes the error to the HTTP response with appropriate status code 21 | func WriteError(c echo.Context, err error) error { 22 | code, message := ErrorCode(err), ErrorMessage(err) 23 | 24 | if debugInfo := ErrorDebugInfo(err); debugInfo != "" { 25 | logger.WithValues("code", code, "error", message).Errorf(debugInfo) 26 | } 27 | 28 | return c.JSON(ErrorStatusCode(code), &HTTPError{Error: message}) 29 | } 30 | 31 | // ErrorStatusCode returns the associated HTTP status code for an application error code. 32 | func ErrorStatusCode(code string) int { 33 | // lookup of application error codes to HTTP status codes. 34 | var codes = map[string]int{ 35 | ECONFLICT: http.StatusConflict, 36 | EINVALID: http.StatusBadRequest, 37 | ENOTFOUND: http.StatusNotFound, 38 | EFORBIDDEN: http.StatusForbidden, 39 | ENOTIMPLEMENTED: http.StatusNotImplemented, 40 | EUNAUTHORIZED: http.StatusUnauthorized, 41 | EINTERNAL: http.StatusInternalServerError, 42 | } 43 | 44 | if v, ok := codes[code]; ok { 45 | return v 46 | } 47 | 48 | return http.StatusInternalServerError 49 | } 50 | -------------------------------------------------------------------------------- /fixtures/datasources/folder_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-pass 5 | spec: 6 | interval: 300 7 | folder: 8 | - path: /etc/ 9 | name: Check for updated /etc files 10 | filter: 11 | # use the last known max, or 60 days ago if no last known max 12 | since: | 13 | {{- if last_result.results.max }} 14 | {{ last_result.results.max }} 15 | {{- else}} 16 | now-60d 17 | {{- end}} 18 | transform: 19 | # Save the newest modified time to the results, overriding the full file listing that would normally be saved 20 | # if no new files detected, use the last known max 21 | expr: | 22 | { 23 | "detail": { 24 | "max": string(results.?newest.modified.orValue(last_result().results.?max.orValue("now-60d"))), 25 | } 26 | }.toJSON() 27 | display: 28 | expr: results.?files.orValue([]).map(i, i.name).join(", ") 29 | test: 30 | expr: results.?files.orValue([]).size() > 0 31 | metrics: 32 | - name: new_files 33 | value: results.?files.orValue([]).size() 34 | type: counter 35 | --- 36 | apiVersion: canaries.flanksource.com/v1 37 | kind: Canary 38 | metadata: 39 | name: folder-pass-empty 40 | spec: 41 | interval: 300 42 | folder: 43 | - name: folder-nil-handling 44 | path: /some/folder/that/does/not/exist 45 | test: 46 | expr: results.files.size() == 0 47 | -------------------------------------------------------------------------------- /pkg/topology/component_config_test.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/pkg" 5 | "github.com/flanksource/duty/query" 6 | "github.com/flanksource/duty/types" 7 | ginkgo "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = ginkgo.Describe("Topology configs", ginkgo.Ordered, func() { 12 | topology := pkg.Topology{Name: "Topology ComponentConfig"} 13 | component := pkg.Component{ 14 | Name: "Component with configs", 15 | Configs: types.ConfigQueries{ 16 | { 17 | Tags: map[string]string{ 18 | "environment": "production", 19 | }, 20 | }, 21 | }, 22 | } 23 | 24 | ginkgo.BeforeAll(func() { 25 | err := DefaultContext.DB().Save(&topology).Error 26 | Expect(err).To(BeNil()) 27 | 28 | component.TopologyID = topology.ID 29 | err = DefaultContext.DB().Save(&component).Error 30 | Expect(err).To(BeNil()) 31 | }) 32 | 33 | ginkgo.It("should create relationships", func() { 34 | ComponentConfigRun.Context = DefaultContext 35 | ComponentConfigRun.Trace = true 36 | ComponentConfigRun.Run() 37 | expectJobToPass(ComponentConfigRun) 38 | 39 | cr, err := component.GetConfigs(DefaultContext) 40 | Expect(err).To(BeNil()) 41 | Expect(len(cr)).Should(BeNumerically(">", 1)) 42 | 43 | ci, err := query.GetCachedConfig(DefaultContext, cr[0].ConfigID.String()) 44 | Expect(err).To(BeNil()) 45 | 46 | tags := *ci.Labels 47 | Expect(tags["environment"]).To(Equal("production")) 48 | 49 | Expect(len(cr)).Should(BeNumerically(">", 2)) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /checks/folder_sftp.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/flanksource/artifacts" 7 | "github.com/flanksource/artifacts/clients/sftp" 8 | 9 | "github.com/flanksource/canary-checker/api/context" 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg" 12 | ) 13 | 14 | func CheckSFTP(ctx *context.Context, check v1.FolderCheck) pkg.Results { 15 | result := pkg.Success(check, ctx.Canary) 16 | var results pkg.Results 17 | results = append(results, result) 18 | 19 | foundConn, err := check.SFTPConnection.HydrateConnection(ctx) 20 | if err != nil { 21 | return results.Failf("failed to populate SFTP connection: %v", err) 22 | } 23 | 24 | auth := check.SFTPConnection.Authentication 25 | if !foundConn { 26 | auth, err = ctx.GetAuthValues(check.SFTPConnection.Authentication) 27 | if err != nil { 28 | return results.ErrorMessage(err) 29 | } 30 | } 31 | 32 | client, err := sftp.SSHConnect(fmt.Sprintf("%s:%d", check.SFTPConnection.Host, check.SFTPConnection.GetPort()), auth.GetUsername(), auth.GetPassword()) 33 | if err != nil { 34 | return results.ErrorMessage(err) 35 | } 36 | defer client.Close() 37 | 38 | session := artifacts.Filesystem(client) 39 | folders, err := genericFolderCheck(session, check.Path, check.Recursive, check.Filter) 40 | if err != nil { 41 | return results.ErrorMessage(err) 42 | } 43 | result.AddDetails(folders) 44 | 45 | if test := folders.Test(check.FolderTest); test != "" { 46 | return results.Failf(test) 47 | } 48 | 49 | return results 50 | } 51 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_service_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-svc-test 6 | namespace: default 7 | labels: 8 | "Expected-Fail": "false" 9 | spec: 10 | schedule: "@every 5m" 11 | kubernetesResource: 12 | - name: service accessibility test 13 | namespace: default 14 | description: "deploy httpbin & check that it's accessible via its service" 15 | waitFor: 16 | expr: 'dyn(resources).all(r, k8s.isReady(r))' 17 | interval: 2s 18 | timeout: 2m 19 | resources: 20 | - apiVersion: v1 21 | kind: Pod 22 | metadata: 23 | name: httpbin-pod-1 24 | namespace: default 25 | labels: 26 | app: httpbin-pod-1 27 | spec: 28 | containers: 29 | - name: httpbin 30 | image: "kennethreitz/httpbin:latest" 31 | ports: 32 | - containerPort: 80 33 | - apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: httpbin-svc 37 | namespace: default 38 | spec: 39 | selector: 40 | app: httpbin-pod-1 41 | ports: 42 | - port: 80 43 | targetPort: 80 44 | checks: 45 | - http: 46 | - name: Call httpbin service 47 | url: "http://httpbin-svc.default.svc" 48 | checkRetries: 49 | delay: 2s 50 | interval: 3s 51 | timeout: 2m 52 | -------------------------------------------------------------------------------- /.github/workflows/e2e-operator.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - v* 5 | branches: 6 | - master 7 | paths: 8 | - "**.go" 9 | - "Makefile" 10 | - "**.yaml" 11 | - "**.yml" 12 | - "test/**" 13 | pull_request: 14 | paths: 15 | - "**.go" 16 | - "Makefile" 17 | - "**.yaml" 18 | - "**.yml" 19 | - "test/**" 20 | name: Operator E2E Test 21 | permissions: 22 | contents: read 23 | jobs: 24 | test: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Install Go 28 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 29 | with: 30 | go-version: 1.22.x 31 | 32 | - name: Checkout code 33 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 34 | 35 | - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 36 | with: 37 | path: | 38 | ~/go/pkg/mod 39 | ~/.cache/go-build 40 | .bin 41 | key: cache-${{ hashFiles('**/go.sum') }}-${{ hashFiles('.bin/*') }} 42 | restore-keys: | 43 | cache- 44 | 45 | - run: make bin 46 | 47 | - name: Set up Kind & Kubectl 48 | uses: helm/kind-action@v1.10.0 49 | with: 50 | version: v0.21.0 51 | cluster_name: kind-test 52 | 53 | - name: Wait for cluster to be ready 54 | run: | 55 | kubectl wait --for=condition=Ready nodes --all --timeout=300s 56 | 57 | - name: Test 58 | run: ./test/e2e-operator.sh 59 | -------------------------------------------------------------------------------- /pkg/topology/query.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/flanksource/commons/collections" 9 | dutyContext "github.com/flanksource/duty/context" 10 | dutyQuery "github.com/flanksource/duty/query" 11 | ) 12 | 13 | const DefaultDepth = 3 14 | 15 | func NewTopologyParams(values url.Values) dutyQuery.TopologyOptions { 16 | parseItems := func(items string) []string { 17 | if strings.TrimSpace(items) == "" { 18 | return nil 19 | } 20 | return strings.Split(strings.TrimSpace(items), ",") 21 | } 22 | 23 | var labels map[string]string 24 | if values.Get("labels") != "" { 25 | labels = collections.KeyValueSliceToMap(strings.Split(values.Get("labels"), ",")) 26 | } 27 | 28 | var err error 29 | var depth = DefaultDepth 30 | if depthStr := values.Get("depth"); depthStr != "" { 31 | depth, err = strconv.Atoi(depthStr) 32 | if err != nil { 33 | depth = DefaultDepth 34 | } 35 | } 36 | return dutyQuery.TopologyOptions{ 37 | ID: values.Get("id"), 38 | Owner: values.Get("owner"), 39 | Labels: labels, 40 | Status: parseItems(values.Get("status")), 41 | Depth: depth, 42 | Types: parseItems(values.Get("type")), 43 | Flatten: values.Get("flatten") == "true", 44 | SortBy: dutyQuery.TopologyQuerySortBy(values.Get("sortBy")), 45 | SortOrder: values.Get("sortOrder"), 46 | NoCache: values.Has("noCache") || values.Has("no-cache"), 47 | } 48 | } 49 | 50 | func Query(ctx dutyContext.Context, params dutyQuery.TopologyOptions) (*dutyQuery.TopologyResponse, error) { 51 | return dutyQuery.Topology(ctx, params) 52 | } 53 | -------------------------------------------------------------------------------- /fixtures/git/_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if ! which mergestat > /dev/null; then 6 | if $(uname -a | grep -q Darwin); then 7 | curl -L https://github.com/flanksource/askgit/releases/download/v0.61.0-flanksource.1/mergestat-macos-amd64.tar.gz -o mergestat.tar.gz 8 | sudo tar xf mergestat.tar.gz -C /usr/local/bin/ 9 | 10 | else 11 | curl -L https://github.com/flanksource/askgit/releases/download/v0.61.0-flanksource.1/mergestat-linux-amd64.tar.gz -o mergestat.tar.gz 12 | sudo tar xf mergestat.tar.gz -C /usr/local/bin/ 13 | fi 14 | fi 15 | 16 | 17 | kubectl create namespace canaries || true 18 | 19 | # creating a GITHUB_TOKEN Secret 20 | if [[ -z "${GH_TOKEN}" ]]; then 21 | printf "\nEnvironment variable for github token (GH_TOKEN) is missing!!!\n" 22 | else 23 | printf "\nCreating secret from github token ending with '${GH_TOKEN:(-8)}'\n" 24 | kubectl create secret generic github-token --from-literal=GITHUB_TOKEN="${GH_TOKEN}" --namespace canaries 25 | fi 26 | 27 | helm repo add gitea-charts https://dl.gitea.io/charts 28 | helm repo update 29 | helm install gitea gitea-charts/gitea -f fixtures/git/gitea.values --create-namespace --namespace gitea --wait 30 | 31 | kubectl port-forward svc/gitea-http -n gitea 3001:3000 & 32 | PID=$! 33 | 34 | sleep 5 35 | 36 | curl -vvv -u gitea_admin:admin -H "Content-Type: application/json" http://localhost:3001/api/v1/user/repos -d '{"name":"test_repo","auto_init":true}' 37 | 38 | kill $PID 39 | 40 | kubectl create secret generic gitea --from-literal=username=gitea_admin --from-literal=password=admin --from-literal=url=http://gitea-http.gitea.svc:3000/gitea_admin/test_repo.git --namespace canaries 41 | -------------------------------------------------------------------------------- /api/v1/db_types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/clause" 12 | "gorm.io/gorm/schema" 13 | ) 14 | 15 | const ( 16 | SQLServerType = "sqlserver" 17 | PostgresType = "postgres" 18 | SqliteType = "sqlite" 19 | text = "TEXT" 20 | jsonType = "json" 21 | jsonbType = "JSONB" 22 | nvarcharType = "NVARCHAR(MAX)" 23 | ) 24 | 25 | type ComponentChecks []ComponentCheck 26 | 27 | func (cs ComponentChecks) Value() (driver.Value, error) { 28 | if len(cs) == 0 { 29 | return []byte("[]"), nil 30 | } 31 | return json.Marshal(cs) 32 | } 33 | 34 | func (cs *ComponentChecks) Scan(val interface{}) error { 35 | if val == nil { 36 | *cs = ComponentChecks{} 37 | return nil 38 | } 39 | var ba []byte 40 | switch v := val.(type) { 41 | case []byte: 42 | ba = v 43 | default: 44 | return errors.New(fmt.Sprint("Failed to unmarshal componentChecks value:", val)) 45 | } 46 | return json.Unmarshal(ba, cs) 47 | } 48 | 49 | // GormDataType gorm common data type 50 | func (cs ComponentChecks) GormDataType() string { 51 | return "componentChecks" 52 | } 53 | 54 | // GormDBDataType gorm db data type 55 | func (ComponentChecks) GormDBDataType(db *gorm.DB, field *schema.Field) string { 56 | switch db.Dialector.Name() { 57 | case SqliteType: 58 | return jsonType 59 | case PostgresType: 60 | return jsonbType 61 | case SQLServerType: 62 | return nvarcharType 63 | } 64 | return "" 65 | } 66 | 67 | func (cs ComponentChecks) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { 68 | data, _ := json.Marshal(cs) 69 | return gorm.Expr("?", string(data)) 70 | } 71 | -------------------------------------------------------------------------------- /fixtures/minimal/cel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: cel-test 5 | spec: 6 | interval: 30 7 | exec: 8 | - name: cel-test 9 | script: "echo 'Hello World'" 10 | display: 11 | expr: | 12 | '\n' + 13 | '3 in [1, 2, 4] = ' + string(3 in [1, 2, 4]) + '\n' + 14 | ['hello', 'mellow'].join('-') + '\n' + 15 | "['a', 'b', 'c', 'd'].slice(1, 3).join(',') = " + ['a', 'b', 'c', 'd'].slice(1, 3).join(',') + '\n' + 16 | '"apple".matches("^a.*e$") = ' + string("apple".matches("^a.*e$")) + '\n' + 17 | '"world".startsWith("wo") = ' + string("world".startsWith("wo")) + '\n' + 18 | '"cherry".contains("err") = ' + string("cherry".contains("err")) + '\n' + 19 | " 'TacoCat'.lowerAscii() = " + 'TacoCat'.lowerAscii() + '\n' + 20 | 'duration("30m") = ' + string(duration("30m").getSeconds()) + '\n' + 21 | 'HumanDuration(23212) = ' + HumanDuration(3600) + '\n' 22 | + 'HumanSize(1048576) = ' + HumanSize(1048576) + '\n' 23 | + 'SemverCompare("1.2.3", "1.2.4") = ' + string(SemverCompare("1.2.3", "1.2.4")) + '\n' 24 | + 'timestamp("1986-12-18T10:00:20.021-05:00") = ' + string(timestamp("1986-12-18T10:00:20.021-05:00").getDayOfMonth()) + '\n' 25 | + 'timestamp("1972-01-01T10:00:20.021-05:00") = ' + HumanDuration(time.Now() - timestamp("1986-12-18T10:00:20.021-05:00")) + '\n' 26 | + string(uuid.IsValid(uuid.V4())) + '\n' 27 | + crypto.SHA384("hello world") + '\n' 28 | + time.ZoneName() + '\n' 29 | + string(time.ZoneOffset()) + '\n' 30 | + string(time.Parse("2006-01-02", "2023-09-26")) 31 | -------------------------------------------------------------------------------- /fixtures/datasources/s3_bucket_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-bucket-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | folder: 10 | # Check for any mysql backup not older than 7 days and min size 100 bytes 11 | - path: s3://tests-e2e-1 12 | name: mysql backup check 13 | awsConnection: 14 | accessKey: 15 | valueFrom: 16 | secretKeyRef: 17 | name: aws-credentials 18 | key: AWS_ACCESS_KEY_ID 19 | secretKey: 20 | valueFrom: 21 | secretKeyRef: 22 | name: aws-credentials 23 | key: AWS_SECRET_ACCESS_KEY 24 | region: "minio" 25 | endpoint: "http://minio.minio:9000" 26 | usePathStyle: true 27 | skipTLSVerify: true 28 | filter: 29 | regex: "^mysql\\/backups\\/(.*)\\/mysql.zip$" 30 | maxAge: 7d 31 | minSize: 100b 32 | 33 | # Check for any pg backup not older than 3 days and min size 20 bytes 34 | - path: s3://tests-e2e-1 35 | name: mysql retension backup check 36 | awsConnection: 37 | accessKey: 38 | valueFrom: 39 | secretKeyRef: 40 | name: aws-credentials 41 | key: AWS_ACCESS_KEY_ID 42 | secretKey: 43 | valueFrom: 44 | secretKeyRef: 45 | name: aws-credentials 46 | key: AWS_SECRET_ACCESS_KEY 47 | region: "minio" 48 | endpoint: "http://minio.minio:9000" 49 | usePathStyle: true 50 | skipTLSVerify: true 51 | filter: 52 | regex: "pg\\/backups\\/(.*)\\/backup.zip$" 53 | maxAge: 3d 54 | minSize: 100b 55 | -------------------------------------------------------------------------------- /pkg/jobs/jobs.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | 6 | "github.com/flanksource/canary-checker/pkg/db" 7 | canaryJobs "github.com/flanksource/canary-checker/pkg/jobs/canary" 8 | topologyJobs "github.com/flanksource/canary-checker/pkg/jobs/topology" 9 | "github.com/flanksource/canary-checker/pkg/topology" 10 | "github.com/flanksource/commons/logger" 11 | "github.com/flanksource/duty/job" 12 | dutyQuery "github.com/flanksource/duty/query" 13 | "github.com/robfig/cron/v3" 14 | ) 15 | 16 | var FuncScheduler = cron.New() 17 | 18 | func Start() { 19 | logger.Infof("Starting jobs ...") 20 | 21 | if canaryJobs.UpstreamConf.Valid() { 22 | for _, j := range canaryJobs.UpstreamJobs { 23 | var job = j 24 | job.Context = context.DefaultContext 25 | if err := job.AddToScheduler(FuncScheduler); err != nil { 26 | logger.Errorf(err.Error()) 27 | } 28 | } 29 | } 30 | 31 | for _, j := range db.CheckStatusJobs { 32 | var job = j 33 | job.Context = context.DefaultContext 34 | if err := job.AddToScheduler(FuncScheduler); err != nil { 35 | logger.Errorf(err.Error()) 36 | } 37 | } 38 | 39 | for _, j := range topology.Jobs { 40 | var job = j 41 | job.Context = context.DefaultContext 42 | if err := job.AddToScheduler(FuncScheduler); err != nil { 43 | logger.Errorf(err.Error()) 44 | } 45 | } 46 | 47 | for _, j := range []*job.Job{topologyJobs.CleanupDeletedTopologyComponents, topologyJobs.SyncTopology, canaryJobs.SyncCanaryJobs, canaryJobs.CleanupDeletedCanaryChecks, dutyQuery.SyncComponentCacheJob} { 48 | var job = j 49 | job.Context = context.DefaultContext 50 | if err := job.AddToScheduler(FuncScheduler); err != nil { 51 | logger.Errorf(err.Error()) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /checks/azure_devops_test.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/microsoft/azure-devops-go-api/azuredevops/v7/pipelines" 7 | "github.com/samber/lo" 8 | ) 9 | 10 | func TestMatchPipelineVariables(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | want map[string]string 14 | got *map[string]pipelines.Variable 15 | wantResult bool 16 | }{ 17 | { 18 | name: "Empty want and got", 19 | want: map[string]string{}, 20 | got: &map[string]pipelines.Variable{}, 21 | wantResult: true, 22 | }, 23 | { 24 | name: "Equal want and got", 25 | want: map[string]string{ 26 | "key1": "value1", 27 | "key2": "value2", 28 | }, 29 | got: &map[string]pipelines.Variable{ 30 | "key1": {Value: lo.ToPtr("value1")}, 31 | "key2": {Value: lo.ToPtr("value2")}, 32 | }, 33 | wantResult: true, 34 | }, 35 | { 36 | name: "Missing key in got", 37 | want: map[string]string{ 38 | "key1": "value1", 39 | "key2": "value2", 40 | }, 41 | got: &map[string]pipelines.Variable{ 42 | "key1": {Value: lo.ToPtr("value1")}, 43 | }, 44 | wantResult: false, 45 | }, 46 | { 47 | name: "Different value in got", 48 | want: map[string]string{ 49 | "key1": "value1", 50 | "key2": "value2", 51 | }, 52 | got: &map[string]pipelines.Variable{ 53 | "key1": {Value: lo.ToPtr("value1")}, 54 | "key2": {Value: lo.ToPtr("value3")}, 55 | }, 56 | wantResult: false, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | if gotResult := matchPipelineVariables(tt.want, tt.got); gotResult != tt.wantResult { 63 | t.Errorf("matchPipelineVariables() = %v, want %v", gotResult, tt.wantResult) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /checks/mongodb.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | gocontext "context" 5 | "time" 6 | 7 | "github.com/flanksource/canary-checker/api/context" 8 | "github.com/flanksource/canary-checker/api/external" 9 | 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | "go.mongodb.org/mongo-driver/mongo/readpref" 15 | ) 16 | 17 | type MongoDBChecker struct { 18 | } 19 | 20 | func (c *MongoDBChecker) Type() string { 21 | return "mongodb" 22 | } 23 | 24 | func (c *MongoDBChecker) Run(ctx *context.Context) pkg.Results { 25 | var results pkg.Results 26 | for _, conf := range ctx.Canary.Spec.MongoDB { 27 | results = append(results, c.Check(ctx, conf)...) 28 | } 29 | return results 30 | } 31 | 32 | func (c *MongoDBChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 33 | check := extConfig.(v1.MongoDBCheck) 34 | result := pkg.Success(check, ctx.Canary) 35 | var results pkg.Results 36 | results = append(results, result) 37 | var err error 38 | 39 | connection, err := ctx.GetConnection(check.Connection) 40 | if err != nil { 41 | return results.Failf("error getting connection: %v", err) 42 | } 43 | 44 | opts := options.Client(). 45 | ApplyURI(connection.URL). 46 | SetConnectTimeout(3 * time.Second). 47 | SetSocketTimeout(3 * time.Second) 48 | 49 | _ctx, cancel := gocontext.WithTimeout(ctx, 5*time.Second) 50 | defer cancel() 51 | 52 | client, err := mongo.Connect(_ctx, opts) 53 | if err != nil { 54 | return results.ErrorMessage(err) 55 | } 56 | defer client.Disconnect(ctx) //nolint: errcheck 57 | 58 | err = client.Ping(_ctx, readpref.Primary()) 59 | if err != nil { 60 | return results.ErrorMessage(err) 61 | } 62 | 63 | return results 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/datasources/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flanksource/canary-checker/fixtures-crd/datasources 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.22.1 7 | github.com/aws/aws-sdk-go-v2/config v1.22.0 8 | github.com/aws/aws-sdk-go-v2/credentials v1.15.1 9 | github.com/aws/aws-sdk-go-v2/service/s3 v1.42.0 10 | github.com/flanksource/commons v1.17.1 11 | github.com/ncw/swift v1.0.53 12 | github.com/pkg/errors v0.9.1 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.0 // indirect 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.2 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.1 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.1 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.5.0 // indirect 21 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.1 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.0 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.1 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.1 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.17.0 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.0 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sts v1.25.0 // indirect 29 | github.com/aws/smithy-go v1.16.0 // indirect 30 | github.com/kr/pretty v0.3.1 // indirect 31 | github.com/kr/text v0.2.0 // indirect 32 | github.com/rogpeppe/go-internal v1.11.0 // indirect 33 | github.com/sirupsen/logrus v1.9.3 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | go.uber.org/multierr v1.11.0 // indirect 36 | go.uber.org/zap v1.26.0 // indirect 37 | golang.org/x/sys v0.14.0 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /pkg/dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "time" 9 | 10 | "github.com/allegro/bigcache" 11 | "github.com/eko/gocache/lib/v4/marshaler" 12 | bcstore "github.com/eko/gocache/store/bigcache/v4" 13 | "github.com/flanksource/commons/logger" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type Cache struct { 18 | *marshaler.Marshaler 19 | } 20 | 21 | var cache *Cache 22 | 23 | func NewCache() (*Cache, error) { 24 | bigcacheClient, _ := bigcache.NewBigCache(bigcache.DefaultConfig(60 * time.Minute)) 25 | bigcacheStore := bcstore.NewBigcache(bigcacheClient) 26 | return &Cache{marshaler.New(bigcacheStore)}, nil 27 | } 28 | 29 | func init() { 30 | cache, _ = NewCache() 31 | } 32 | 33 | type IPs []net.IP 34 | 35 | func CacheLookup(recordType, hostname string) ([]net.IP, error) { 36 | var ips IPs 37 | key := fmt.Sprintf("%s:%s", recordType, hostname) 38 | 39 | if _, err := cache.Get(context.TODO(), key, &ips); err == nil { 40 | return ips, nil 41 | } 42 | 43 | ips, err := Lookup(recordType, hostname) 44 | if err != nil { 45 | return nil, err 46 | } 47 | err = cache.Set(context.TODO(), key, ips, nil) 48 | return ips, err 49 | } 50 | 51 | func Lookup(recordType, hostname string) ([]net.IP, error) { 52 | host := hostname 53 | if url, err := url.Parse(hostname); err != nil { 54 | return nil, errors.Wrapf(err, "invalid IP/URL: %s", hostname) 55 | } else if url.Hostname() != "" { 56 | host = url.Hostname() 57 | } 58 | 59 | if ip := net.ParseIP(host); ip != nil { 60 | return []net.IP{ip}, nil 61 | } 62 | 63 | ips, err := net.LookupIP(host) 64 | if err != nil { 65 | return nil, errors.Wrapf(err, "lookup of %s failed", host) 66 | } 67 | var ipv4 []net.IP 68 | for _, ip := range ips { 69 | if ip.To4() != nil { 70 | ipv4 = append(ipv4, ip) 71 | } 72 | } 73 | logger.Debugf("%s => %v", host, ipv4) 74 | return ipv4, nil 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/aws-exec.yml: -------------------------------------------------------------------------------- 1 | name: AWS-exec-test 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | permissions: 8 | contents: read 9 | id-token: write 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | suite: 17 | - aws/minimal 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Install Go 21 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 22 | with: 23 | go-version: 1.22.x 24 | - name: Checkout code 25 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 26 | - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 27 | with: 28 | path: | 29 | ~/go/pkg/mod 30 | ~/.cache/go-build 31 | .bin 32 | key: cache-${{ hashFiles('**/go.sum') }}-${{ hashFiles('.bin/*') }} 33 | restore-keys: | 34 | cache- 35 | - name: Build 36 | run: make bin 37 | - name: Configure AWS Credentials 38 | uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 39 | with: 40 | role-to-assume: arn:aws:iam::765618022540:role/canary-checker-github-iam-Role-N9JG51I5V3JJ 41 | aws-region: us-east-1 42 | role-duration-seconds: 1800 # 30 minutes 43 | - name: Test 44 | env: 45 | KUBERNETES_VERSION: v1.20.7 46 | GH_TOKEN: ${{ secrets.CHECKRUNS_TOKEN }} 47 | run: ./test/e2e.sh fixtures/${{matrix.suite}} 48 | - name: Publish Unit Test Results 49 | uses: EnricoMi/publish-unit-test-result-action@f355d34d53ad4e7f506f699478db2dd71da9de5f # v2.15.1 50 | if: always() && github.event.repository.fork == 'false' 51 | with: 52 | files: test/test-results.xml 53 | check_name: E2E - ${{matrix.suite}} 54 | -------------------------------------------------------------------------------- /checks/database_backup.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/prometheus/client_golang/prometheus" 6 | 7 | "github.com/flanksource/canary-checker/api/context" 8 | "github.com/flanksource/canary-checker/api/external" 9 | v1 "github.com/flanksource/canary-checker/api/v1" 10 | "github.com/flanksource/canary-checker/pkg" 11 | ) 12 | 13 | var ( 14 | databaseScanObjectCount = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Name: "canary_check_database_backup_scan_count", 17 | Help: "The total number of objects", 18 | }, 19 | []string{"project", "instance"}, 20 | ) 21 | databaseScanFailCount = prometheus.NewCounterVec( 22 | prometheus.CounterOpts{ 23 | Name: "canary_check_database_backup_fail_count", 24 | Help: "The number of failed backups detected", 25 | }, 26 | []string{"project", "instance"}, 27 | ) 28 | ) 29 | 30 | func init() { 31 | prometheus.MustRegister(databaseScanObjectCount, databaseScanFailCount) 32 | } 33 | 34 | type DatabaseBackupChecker struct { 35 | } 36 | 37 | func (c *DatabaseBackupChecker) Type() string { 38 | return "databasebackupcheck" 39 | } 40 | 41 | func (c *DatabaseBackupChecker) Run(ctx *context.Context) pkg.Results { 42 | var results pkg.Results 43 | for _, conf := range ctx.Canary.Spec.DatabaseBackup { 44 | results = append(results, c.Check(ctx, conf)...) 45 | } 46 | return results 47 | } 48 | 49 | func (c *DatabaseBackupChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 50 | check := extConfig.(v1.DatabaseBackupCheck) 51 | switch { 52 | case check.GCP != nil: 53 | return GCPDatabaseBackupCheck(ctx, check) 54 | default: 55 | return FailDatabaseBackupParse(ctx, check) 56 | } 57 | } 58 | 59 | func FailDatabaseBackupParse(ctx *context.Context, check v1.DatabaseBackupCheck) pkg.Results { 60 | result := pkg.Fail(check, ctx.Canary) 61 | var results pkg.Results 62 | results = append(results, result) 63 | return results.ErrorMessage(errors.New("Could not parse databaseBackup input")) 64 | } 65 | -------------------------------------------------------------------------------- /checks/tcp.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | "github.com/flanksource/canary-checker/api/context" 10 | 11 | "github.com/flanksource/canary-checker/api/external" 12 | v1 "github.com/flanksource/canary-checker/api/v1" 13 | "github.com/flanksource/canary-checker/pkg" 14 | ) 15 | 16 | // TCPChecker checks if the given port is open on the given host 17 | type TCPChecker struct{} 18 | 19 | // NewTCPChecker creates and returns a pointer to a TCPChecker 20 | func NewTCPChecker() *TCPChecker { 21 | return &TCPChecker{} 22 | } 23 | 24 | // Run executes tcp checks for the given config, returning results 25 | func (t *TCPChecker) Run(ctx *context.Context) pkg.Results { 26 | var results pkg.Results 27 | for _, c := range ctx.Canary.Spec.TCP { 28 | results = append(results, t.Check(ctx, c)...) 29 | } 30 | return results 31 | } 32 | 33 | // Check performs a single tcp check, returning a checkResult 34 | func (t *TCPChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 35 | c := extConfig.(v1.TCPCheck) 36 | result := pkg.Success(c, ctx.Canary) 37 | var results pkg.Results 38 | results = append(results, result) 39 | 40 | addr, port, err := extractAddrAndPort(c.Endpoint) 41 | if err != nil { 42 | return results.ErrorMessage(err) 43 | } 44 | 45 | timeout := time.Millisecond * time.Duration(c.ThresholdMillis) 46 | conn, err := net.DialTimeout("tcp", net.JoinHostPort(addr, port), timeout) 47 | if err != nil { 48 | return results.Failf("Connection error: %s", err) 49 | } 50 | if conn != nil { 51 | defer conn.Close() 52 | } 53 | return results 54 | } 55 | 56 | func extractAddrAndPort(e string) (string, string, error) { 57 | s := strings.Split(e, ":") 58 | if len(s) != 2 { 59 | return "", "", fmt.Errorf(formatErrorMsg(e)) 60 | } 61 | return s[0], s[1], nil 62 | } 63 | 64 | func formatErrorMsg(f string) string { 65 | return fmt.Sprintf("Incorrect endpoint format: %s should be ADDRESS:PORT", f) 66 | } 67 | 68 | // Type returns the type 69 | func (t *TCPChecker) Type() string { 70 | return "tcp" 71 | } 72 | -------------------------------------------------------------------------------- /fixtures/minimal/dns_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: dns-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | interval: 30 9 | dns: 10 | - server: 8.8.8.8 11 | name: A record query 12 | port: 53 13 | query: "1.2.3.4.nip.io" 14 | querytype: "A" 15 | minrecords: 1 16 | exactreply: ["8.8.8.8"] 17 | timeout: 10 18 | thresholdMillis: 1000 19 | - server: 8.8.8.8 20 | port: 53 21 | query: "8.8.8.8" 22 | name: ptr query 23 | querytype: "PTR" 24 | minrecords: 10 25 | exactreply: ["dns.google."] 26 | timeout: 10 27 | thresholdMillis: 1000 28 | - server: 8.8.8.8 29 | port: 53 30 | name: cname query 31 | query: "dns.google" 32 | querytype: "CNAME" 33 | minrecords: 1 34 | exactreply: ["wrong.google."] 35 | timeout: 10 36 | thresholdMillis: 1000 37 | - server: 8.8.8.8 38 | port: 53 39 | name: mx query 40 | query: "flanksource.com" 41 | querytype: "MX" 42 | minrecords: 1 43 | exactreply: 44 | - "aspmx.l.google.com. 1" 45 | - "alt1.aspmx.l.google.com. 5" 46 | - "alt2.aspmx.l.google.com. 5" 47 | timeout: 10 48 | thresholdMillis: 1000 49 | - server: 8.8.8.8 50 | port: 53 51 | name: txt query 52 | query: "flanksource.com" 53 | querytype: "TXT" 54 | minrecords: 5 55 | exactreply: ["google-site-verification=IIE1aJuvqseLUKSXSIhu2O2lgdU_d8csfJjjIQVc-q0"] 56 | timeout: 10 57 | thresholdMillis: 1000 58 | - server: 8.8.8.8 59 | name: NS query 60 | port: 53 61 | query: "flanksource.com" 62 | querytype: "NS" 63 | minrecords: 1 64 | exactreply: 65 | - "ns-91.awsdns-11.com." 66 | timeout: 10 67 | thresholdMillis: 1000 68 | # - server: 8.8.8.8 69 | # port: 53 70 | # querytype: "SRV" 71 | # query: "_test._tcp.test" 72 | # timeout: 10 73 | # srvReply: 74 | # target: "" 75 | # port: 0 76 | # priority: 0 77 | # weight: 0* 78 | -------------------------------------------------------------------------------- /checks/redis.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/flanksource/canary-checker/api/context" 7 | 8 | "github.com/flanksource/canary-checker/api/external" 9 | v1 "github.com/flanksource/canary-checker/api/v1" 10 | "github.com/flanksource/canary-checker/pkg" 11 | "github.com/go-redis/redis/v8" 12 | ) 13 | 14 | func init() { 15 | //register metrics here 16 | } 17 | 18 | type RedisChecker struct { 19 | } 20 | 21 | // Type: returns checker type 22 | func (c *RedisChecker) Type() string { 23 | return "redis" 24 | } 25 | 26 | // Run: Check every entry from config according to Checker interface 27 | // Returns check result and metrics 28 | func (c *RedisChecker) Run(ctx *context.Context) pkg.Results { 29 | var results pkg.Results 30 | for _, conf := range ctx.Canary.Spec.Redis { 31 | results = append(results, c.Check(ctx, conf)...) 32 | } 33 | return results 34 | } 35 | 36 | func (c *RedisChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 37 | check := extConfig.(v1.RedisCheck) 38 | result := pkg.Success(check, ctx.Canary) 39 | var results pkg.Results 40 | results = append(results, result) 41 | 42 | var redisOpts *redis.Options 43 | 44 | //nolint:staticcheck 45 | if check.Addr != "" && check.URL == "" { 46 | check.URL = check.Addr 47 | } 48 | 49 | connection, err := ctx.GetConnection(check.Connection) 50 | if err != nil { 51 | return results.Failf("error getting connection: %v", err) 52 | } 53 | 54 | redisOpts = &redis.Options{ 55 | Addr: connection.URL, 56 | Username: connection.Username, 57 | Password: connection.Password, 58 | } 59 | 60 | if check.DB != nil { 61 | redisOpts.DB = *check.DB 62 | } else if db, ok := connection.Properties["db"]; ok { 63 | if dbInt, err := strconv.Atoi(db); nil == err { 64 | redisOpts.DB = dbInt 65 | } 66 | } 67 | 68 | rdb := redis.NewClient(redisOpts) 69 | queryResult, err := rdb.Ping(ctx).Result() 70 | if err != nil { 71 | return results.Failf("failed to execute query %v", err) 72 | } 73 | 74 | if queryResult != "PONG" { 75 | return results.Failf("expected PONG as result, got %s", result) 76 | } 77 | 78 | return results 79 | } 80 | -------------------------------------------------------------------------------- /fixtures/minimal/dns_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: dns-pass 5 | spec: 6 | schedule: "@every 1m" 7 | dns: 8 | - server: 8.8.8.8 9 | name: A record query 10 | port: 53 11 | query: "1.2.3.4.nip.io" 12 | querytype: "A" 13 | minrecords: 1 14 | exactreply: ["1.2.3.4"] 15 | timeout: 100 16 | thresholdMillis: 1000 17 | - server: 1.1.1.1 18 | port: 53 19 | name: ptr query 20 | query: "1.1.1.1" 21 | querytype: "PTR" 22 | minrecords: 1 23 | exactreply: ["one.one.one.one."] 24 | timeout: 100 25 | thresholdMillis: 100 26 | - server: 8.8.8.8 27 | port: 53 28 | name: cname query 29 | query: "dns.google" 30 | querytype: "CNAME" 31 | minrecords: 1 32 | exactreply: ["dns.google."] 33 | timeout: 100 34 | thresholdMillis: 1000 35 | - server: 8.8.8.8 36 | port: 53 37 | query: "flanksource.com" 38 | name: mx query 39 | querytype: "MX" 40 | minrecords: 1 41 | exactreply: 42 | - "aspmx.l.google.com. 1" 43 | - "alt1.aspmx.l.google.com. 5" 44 | - "alt2.aspmx.l.google.com. 5" 45 | - "aspmx3.googlemail.com. 10" 46 | - "aspmx2.googlemail.com. 10" 47 | timeout: 100 48 | thresholdMillis: 1000 49 | - server: 8.8.8.8 50 | port: 53 51 | name: txt query 52 | query: "flanksource.com" 53 | querytype: "TXT" 54 | minrecords: 1 55 | timeout: 100 56 | thresholdMillis: 1000 57 | - server: 8.8.8.8 58 | port: 53 59 | name: NS query 60 | query: "flanksource.com" 61 | querytype: "NS" 62 | minrecords: 1 63 | exactreply: 64 | - "ns-91.awsdns-11.com." 65 | - "ns-908.awsdns-49.net." 66 | - "ns-1450.awsdns-53.org." 67 | - "ns-1896.awsdns-45.co.uk." 68 | timeout: 100 69 | thresholdMillis: 1000 70 | # - server: 8.8.8.8 71 | # port: 53 72 | # querytype: "SRV" 73 | # query: "_test._tcp.test" 74 | # timeout: 10 75 | # srvReply: 76 | # target: "" 77 | # port: 0 78 | # priority: 0 79 | # weight: 0* 80 | -------------------------------------------------------------------------------- /checks/github.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | osExec "os/exec" 7 | 8 | "github.com/flanksource/canary-checker/api/context" 9 | "github.com/flanksource/canary-checker/api/external" 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg" 12 | ) 13 | 14 | func init() { 15 | //register metrics here 16 | } 17 | 18 | type GitHubChecker struct { 19 | } 20 | 21 | func (c *GitHubChecker) Type() string { 22 | return "github" 23 | } 24 | 25 | func (c *GitHubChecker) Run(ctx *context.Context) pkg.Results { 26 | var results pkg.Results 27 | for _, conf := range ctx.Canary.Spec.GitHub { 28 | results = append(results, c.Check(ctx, conf)...) 29 | } 30 | return results 31 | } 32 | 33 | func (c *GitHubChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 34 | check := extConfig.(v1.GitHubCheck) 35 | result := pkg.Success(check, ctx.Canary) 36 | var results pkg.Results 37 | results = append(results, result) 38 | 39 | var githubToken string 40 | if connection, err := ctx.HydrateConnectionByURL(check.ConnectionName); err != nil { 41 | return results.Failf("failed to find connection for github token %q: %v", check.ConnectionName, err) 42 | } else if connection != nil { 43 | githubToken = connection.Password 44 | } else { 45 | githubToken, err = ctx.GetEnvValueFromCache(check.GithubToken, ctx.GetNamespace()) 46 | if err != nil { 47 | return results.Failf("error fetching github token from env cache: %v", err) 48 | } 49 | } 50 | 51 | askGitCmd := fmt.Sprintf("mergestat \"%v\" --format json", check.Query) 52 | if ctx.IsTrace() { 53 | ctx.Tracef("Executing askgit command: %v", askGitCmd) 54 | } 55 | cmd := osExec.Command("bash", "-c", askGitCmd) 56 | cmd.Env = append(cmd.Env, "GITHUB_TOKEN="+githubToken) 57 | output, err := cmd.CombinedOutput() 58 | if err != nil { 59 | return results.Failf("error executing askgit command. output=%q: %v", output, err) 60 | } 61 | 62 | var rowResults = make([]map[string]any, 0) 63 | err = json.Unmarshal(output, &rowResults) 64 | if err != nil { 65 | return results.Failf("error parsing mergestat result: %v", err) 66 | } 67 | 68 | result.AddDetails(rowResults) 69 | return results 70 | } 71 | -------------------------------------------------------------------------------- /api/v1/conditions.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 4 | 5 | //based upon https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1623-standardize-conditions 6 | 7 | type ConditionStatus string 8 | 9 | const ( 10 | ConditionStatusTrue ConditionStatus = "True" 11 | ConditionStatusFalse ConditionStatus = "False" 12 | ConditionStatusUnknown ConditionStatus = "Unknown" 13 | ) 14 | 15 | type Condition struct { 16 | // Type of condition in CamelCase or in foo.example.com/CamelCase. 17 | // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be 18 | // useful (see .node.status.conditions), the ability to deconflict is important. 19 | // +required 20 | Type string `json:"type" protobuf:"bytes,1,opt,name=type"` 21 | // Status of the condition, one of True, False, Unknown. 22 | // +required 23 | Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status"` 24 | // If set, this represents the .metadata.generation that the condition was set based upon. 25 | // For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date 26 | // with respect to the current state of the instance. 27 | // +optional 28 | ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` 29 | // Last time the condition transitioned from one status to another. 30 | // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 31 | // +required 32 | LastTransitionTime metav1.Time `json:"lastTransitionTime" protobuf:"bytes,4,opt,name=lastTransitionTime"` 33 | // The reason for the condition's last transition in CamelCase. 34 | // The specific API may choose whether or not this field is considered a guaranteed API. 35 | // This field may not be empty. 36 | // +required 37 | Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"` 38 | // A human readable message indicating details about the transition. 39 | // This field may be empty. 40 | // +required 41 | Message string `json:"message" protobuf:"bytes,6,opt,name=message"` 42 | } 43 | -------------------------------------------------------------------------------- /pkg/jobs/canary/suite_test.go: -------------------------------------------------------------------------------- 1 | package canary 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/flanksource/canary-checker/pkg/cache" 11 | "github.com/flanksource/canary-checker/pkg/utils" 12 | "github.com/flanksource/commons/logger" 13 | dutyContext "github.com/flanksource/duty/context" 14 | "github.com/flanksource/duty/tests/setup" 15 | "github.com/labstack/echo/v4" 16 | "github.com/onsi/ginkgo/v2" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var ( 21 | testEchoServer *echo.Echo 22 | testEchoServerPort int 23 | requestCount int 24 | 25 | DefaultContext dutyContext.Context 26 | ) 27 | 28 | func TestCanaryJobs(t *testing.T) { 29 | RegisterFailHandler(ginkgo.Fail) 30 | ginkgo.RunSpecs(t, "Canary Job") 31 | } 32 | 33 | // DelayedResponseHandler waits for "delay" seconds before responding. 34 | // It's used as a test server for HTTP check. 35 | func DelayedResponseHandler(c echo.Context) error { 36 | requestCount++ 37 | logger.Debugf("DelayedResponseHandler called: %d", requestCount) 38 | delayStr := c.QueryParam("delay") 39 | delay, err := strconv.Atoi(delayStr) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | time.Sleep(time.Duration(delay) * time.Second) 45 | return c.String(http.StatusOK, "Done") 46 | } 47 | 48 | var _ = ginkgo.BeforeSuite(func() { 49 | DefaultContext = setup.BeforeSuiteFn().WithTrace() 50 | 51 | cache.PostgresCache = cache.NewPostgresCache(DefaultContext) 52 | 53 | testEchoServer = echo.New() 54 | testEchoServer.GET("/", DelayedResponseHandler) 55 | testEchoServerPort = utils.FreePort() 56 | listenAddr := fmt.Sprintf(":%d", testEchoServerPort) 57 | 58 | go func() { 59 | defer ginkgo.GinkgoRecover() // Required by ginkgo, if an assertion is made in a goroutine. 60 | if err := testEchoServer.Start(listenAddr); err != nil { 61 | if err == http.ErrServerClosed { 62 | logger.Infof("Server closed") 63 | } else { 64 | ginkgo.Fail(fmt.Sprintf("Failed to start test server: %v", err)) 65 | } 66 | } 67 | }() 68 | }) 69 | 70 | var _ = ginkgo.AfterSuite(func() { 71 | logger.Infof("Stopping test echo server") 72 | if err := testEchoServer.Close(); err != nil { 73 | ginkgo.Fail(err.Error()) 74 | } 75 | 76 | setup.AfterSuiteFn() 77 | }) 78 | -------------------------------------------------------------------------------- /checks/ldap.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | "github.com/flanksource/canary-checker/api/context" 7 | 8 | "github.com/flanksource/canary-checker/api/external" 9 | v1 "github.com/flanksource/canary-checker/api/v1" 10 | "github.com/flanksource/canary-checker/pkg" 11 | ldap "github.com/go-ldap/ldap/v3" 12 | ) 13 | 14 | type LdapChecker struct { 15 | } 16 | 17 | // Type: returns checker type 18 | func (c *LdapChecker) Type() string { 19 | return "ldap" 20 | } 21 | 22 | // Run: Check every entry from config according to Checker interface 23 | // Returns check result and metrics 24 | func (c *LdapChecker) Run(ctx *context.Context) pkg.Results { 25 | var results pkg.Results 26 | for _, conf := range ctx.Canary.Spec.LDAP { 27 | results = append(results, c.Check(ctx, conf)...) 28 | } 29 | return results 30 | } 31 | 32 | // CheckConfig : Check every ldap entry for lookup and auth 33 | // Returns check result and metrics 34 | func (c *LdapChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 35 | check := extConfig.(v1.LDAPCheck) 36 | result := pkg.Success(check, ctx.Canary) 37 | var results pkg.Results 38 | var err error 39 | results = append(results, result) 40 | 41 | connection, err := ctx.GetConnection(check.Connection) 42 | if err != nil { 43 | return results.Failf("failed to get connection: %v", err) 44 | } 45 | 46 | if connection.URL == "" { 47 | return results.Failf("Must specify a connection or URL") 48 | } 49 | 50 | ld, err := ldap.DialURL(connection.URL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: check.SkipTLSVerify})) 51 | if err != nil { 52 | return results.Failf("Failed to connect %v", err) 53 | } 54 | 55 | if err := ld.Bind(connection.Username, connection.Password); err != nil { 56 | return results.Failf("Failed to bind using %s %v", connection.Username, err) 57 | } 58 | 59 | req := &ldap.SearchRequest{ 60 | Scope: ldap.ScopeWholeSubtree, 61 | BaseDN: check.BindDN, 62 | Filter: check.UserSearch, 63 | } 64 | res, err := ld.Search(req) 65 | if err != nil { 66 | return results.Failf("Failed to search host %v error: %v", connection.URL, err) 67 | } 68 | 69 | if len(res.Entries) == 0 { 70 | return results.Failf("no results returned") 71 | } 72 | 73 | return results 74 | } 75 | -------------------------------------------------------------------------------- /pkg/jobs/canary/canary_jobs_test.go: -------------------------------------------------------------------------------- 1 | package canary 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | canaryCtx "github.com/flanksource/canary-checker/api/context" 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg/db" 12 | "github.com/flanksource/duty/models" 13 | "github.com/flanksource/duty/tests/setup" 14 | "github.com/flanksource/duty/types" 15 | "github.com/google/uuid" 16 | ginkgo "github.com/onsi/ginkgo/v2" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var _ = ginkgo.Describe("Canary Job sync", ginkgo.Ordered, func() { 21 | var canarySpec v1.CanarySpec 22 | ginkgo.BeforeEach(func() { 23 | canarySpec = v1.CanarySpec{ 24 | Schedule: "@every 1s", 25 | HTTP: []v1.HTTPCheck{ 26 | { 27 | Endpoint: fmt.Sprintf("http://localhost:%d?delay=10", testEchoServerPort), // server only responds after 10 seconds 28 | ResponseCodes: []int{http.StatusOK}, 29 | }, 30 | }, 31 | } 32 | }) 33 | 34 | ginkgo.It("should save a canary spec", func() { 35 | b, err := json.Marshal(canarySpec) 36 | Expect(err).To(BeNil()) 37 | 38 | var spec types.JSON 39 | err = json.Unmarshal(b, &spec) 40 | Expect(err).To(BeNil()) 41 | 42 | canaryM := &models.Canary{ 43 | ID: uuid.New(), 44 | Annotations: map[string]string{ 45 | "trace": "true", 46 | }, 47 | Spec: spec, 48 | Name: "http check", 49 | } 50 | err = DefaultContext.DB().Create(canaryM).Error 51 | Expect(err).To(BeNil()) 52 | 53 | response, err := db.GetAllCanariesForSync(DefaultContext, "") 54 | Expect(err).To(BeNil()) 55 | Expect(len(response)).To(BeNumerically(">=", 1)) 56 | }) 57 | 58 | ginkgo.It("schedule the canary job", func() { 59 | MinimumTimeBetweenCanaryRuns = 0 // reset this for now so it doesn't hinder test with small schedules 60 | SyncCanaryJobs.Context = DefaultContext 61 | canaryCtx.DefaultContext = DefaultContext 62 | SyncCanaryJobs.Run() 63 | setup.ExpectJobToPass(SyncCanaryJobs) 64 | }) 65 | 66 | ginkgo.It("should verify that the endpoint wasn't called more than once after 3 seconds", func() { 67 | time.Sleep(time.Second * 3) 68 | // The job will be called on first schedule and all concurrent jobs would be aborted 69 | Expect(requestCount).To(Equal(1)) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /.github/workflows/helm-test.yml: -------------------------------------------------------------------------------- 1 | name: Helm Test 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 19 | 20 | - name: Kubernetes KinD Cluster 21 | uses: container-tools/kind-action@v1 22 | 23 | - name: Build and push Docker image 24 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 25 | with: 26 | context: . 27 | file: ./build/full/Dockerfile 28 | push: true 29 | tags: localhost:5000/flanksource/canary-checker:latest 30 | cache-from: type=registry,ref=docker.io/flanksource/canary-checker 31 | 32 | - name: Update canary-checker image in helm chart 33 | uses: mikefarah/yq@master 34 | with: 35 | cmd: yq -i e '.global.imageRegistry = "kind-registry:5000"' chart/values.yaml 36 | 37 | - name: Setup Helm 38 | uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 39 | with: 40 | version: v3.11.3 41 | 42 | - name: Package helm chart 43 | run: | 44 | helm dependency build ./chart 45 | helm package ./chart --version 1.0.0 46 | 47 | - name: Install helm chart 48 | run: 'helm install canary-checker canary-checker-1.0.0.tgz -n canary-checker --create-namespace' 49 | 50 | - name: Wait for 30 seconds 51 | run: 'kubectl rollout status deploy/canary-checker -n canary-checker --timeout 5m' 52 | 53 | - name: Check canary-checker pods 54 | run: 'kubectl describe pods -n canary-checker' 55 | 56 | - name: Apply exec fixture 57 | run: 'kubectl apply -f fixtures/minimal/exec_pass.yaml' 58 | 59 | - name: Wait for 60 seconds 60 | run: 'sleep 60' 61 | 62 | - name: Check status 63 | run: | 64 | status=$(kubectl get canaries.canaries.flanksource.com exec-pass -o yaml | yq .status.status) 65 | echo $status 66 | if [[ $status == "Passed" ]]; then 67 | exit 0 68 | else 69 | exit 1 70 | fi 71 | -------------------------------------------------------------------------------- /config/schemas/health_tcp.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://github.com/flanksource/canary-checker/api/v1/tcp-check", 4 | "$ref": "#/$defs/TCPCheck", 5 | "$defs": { 6 | "Labels": { 7 | "additionalProperties": { 8 | "type": "string" 9 | }, 10 | "type": "object" 11 | }, 12 | "MetricLabel": { 13 | "properties": { 14 | "name": { 15 | "type": "string" 16 | }, 17 | "value": { 18 | "type": "string" 19 | }, 20 | "valueExpr": { 21 | "type": "string" 22 | } 23 | }, 24 | "additionalProperties": false, 25 | "type": "object", 26 | "required": [ 27 | "name" 28 | ] 29 | }, 30 | "MetricLabels": { 31 | "items": { 32 | "$ref": "#/$defs/MetricLabel" 33 | }, 34 | "type": "array" 35 | }, 36 | "Metrics": { 37 | "properties": { 38 | "name": { 39 | "type": "string" 40 | }, 41 | "labels": { 42 | "$ref": "#/$defs/MetricLabels" 43 | }, 44 | "type": { 45 | "type": "string" 46 | }, 47 | "value": { 48 | "type": "string" 49 | } 50 | }, 51 | "additionalProperties": false, 52 | "type": "object" 53 | }, 54 | "TCPCheck": { 55 | "properties": { 56 | "description": { 57 | "type": "string" 58 | }, 59 | "name": { 60 | "type": "string" 61 | }, 62 | "namespace": { 63 | "type": "string" 64 | }, 65 | "icon": { 66 | "type": "string" 67 | }, 68 | "labels": { 69 | "$ref": "#/$defs/Labels" 70 | }, 71 | "transformDeleteStrategy": { 72 | "type": "string" 73 | }, 74 | "metrics": { 75 | "items": { 76 | "$ref": "#/$defs/Metrics" 77 | }, 78 | "type": "array" 79 | }, 80 | "endpoint": { 81 | "type": "string" 82 | }, 83 | "thresholdMillis": { 84 | "type": "integer" 85 | } 86 | }, 87 | "additionalProperties": false, 88 | "type": "object", 89 | "required": [ 90 | "name" 91 | ] 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_ingress_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: ingress-test 6 | namespace: default 7 | labels: 8 | "Expected-Fail": "false" 9 | spec: 10 | schedule: "@every 5m" 11 | kubernetesResource: 12 | - name: ingress-accessibility-check 13 | namespace: default 14 | description: "deploy httpbin & check that it's accessible via ingress" 15 | waitFor: 16 | expr: 'dyn(resources).all(r, k8s.isReady(r))' 17 | interval: 2s 18 | timeout: 5m 19 | staticResources: 20 | - apiVersion: networking.k8s.io/v1 21 | kind: Ingress 22 | metadata: 23 | name: httpbin 24 | namespace: default 25 | spec: 26 | rules: 27 | - host: "httpbin.127.0.0.1.nip.io" 28 | http: 29 | paths: 30 | - pathType: Prefix 31 | path: / 32 | backend: 33 | service: 34 | name: httpbin 35 | port: 36 | number: 80 37 | resources: 38 | - apiVersion: v1 39 | kind: Pod 40 | metadata: 41 | name: httpbin 42 | namespace: default 43 | labels: 44 | app: httpbin 45 | spec: 46 | containers: 47 | - name: httpbin 48 | image: "kennethreitz/httpbin:latest" 49 | ports: 50 | - containerPort: 80 51 | - apiVersion: v1 52 | kind: Service 53 | metadata: 54 | name: httpbin 55 | namespace: default 56 | spec: 57 | selector: 58 | app: httpbin 59 | ports: 60 | - port: 80 61 | targetPort: 80 62 | checks: 63 | - http: 64 | - name: Call httpbin via ingress 65 | url: "http://ingress-nginx.ingress-nginx.svc" 66 | headers: 67 | - name: Host 68 | value: "{{(index ((index .staticResources 0).Object.spec.rules) 0).host}}" 69 | checkRetries: 70 | delay: 3s 71 | interval: 2s 72 | timeout: 5m 73 | -------------------------------------------------------------------------------- /pkg/topology/component_check_test.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | v1 "github.com/flanksource/canary-checker/api/v1" 5 | "github.com/flanksource/canary-checker/pkg" 6 | "github.com/flanksource/duty/models" 7 | "github.com/flanksource/duty/types" 8 | "github.com/google/uuid" 9 | ginkgo "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = ginkgo.Describe("Topology checks", ginkgo.Ordered, func() { 14 | topology := pkg.Topology{Name: "Topology ComponentCheck"} 15 | component := pkg.Component{ 16 | Name: "Component", 17 | ComponentChecks: []v1.ComponentCheck{{ 18 | Selector: types.ResourceSelector{ 19 | LabelSelector: "check-target=api", 20 | }, 21 | }}, 22 | } 23 | canary := models.Canary{ 24 | ID: uuid.New(), 25 | Name: "Canary", 26 | Spec: []byte(`{"spec": {}}`), 27 | } 28 | 29 | ginkgo.BeforeAll(func() { 30 | err := DefaultContext.DB().Create(&topology).Error 31 | Expect(err).To(BeNil()) 32 | 33 | component.TopologyID = topology.ID 34 | err = DefaultContext.DB().Create(&component).Error 35 | Expect(err).To(BeNil()) 36 | 37 | err = DefaultContext.DB().Create(&canary).Error 38 | Expect(err).To(BeNil()) 39 | 40 | check1 := pkg.Check{ 41 | Name: "Check-1", 42 | CanaryID: canary.ID, 43 | Labels: map[string]string{ 44 | "check-target": "api", 45 | "name": "check-1", 46 | }, 47 | } 48 | check2 := pkg.Check{ 49 | Name: "Check-2", 50 | CanaryID: canary.ID, 51 | Labels: map[string]string{ 52 | "check-target": "ui", 53 | "name": "check-2", 54 | }, 55 | } 56 | check3 := pkg.Check{ 57 | Name: "Check-3", 58 | CanaryID: canary.ID, 59 | Labels: map[string]string{ 60 | "check-target": "api", 61 | "name": "check-3", 62 | }, 63 | } 64 | 65 | err = DefaultContext.DB().Create([]pkg.Check{check1, check2, check3}).Error 66 | Expect(err).To(BeNil()) 67 | }) 68 | 69 | ginkgo.It("should create check component relationships", func() { 70 | ComponentCheckRun.Context = DefaultContext 71 | ComponentCheckRun.Trace = true 72 | ComponentCheckRun.Run() 73 | expectJobToPass(ComponentCheckRun) 74 | cr, err := component.GetChecks(DefaultContext.DB()) 75 | Expect(err).To(BeNil()) 76 | 77 | // Check-1 and Check-3 should be present but not Check-2 78 | Expect(len(cr)).To(Equal(2)) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /checks/folder_gcs.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | gcs "cloud.google.com/go/storage" 8 | "github.com/flanksource/artifacts" 9 | "github.com/flanksource/canary-checker/api/context" 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg" 12 | "github.com/flanksource/duty/models" 13 | ) 14 | 15 | type GCS struct { 16 | BucketName string 17 | *gcs.Client 18 | } 19 | 20 | func CheckGCSBucket(ctx *context.Context, check v1.FolderCheck) pkg.Results { 21 | result := pkg.Success(check, ctx.Canary) 22 | var results pkg.Results 23 | results = append(results, result) 24 | 25 | if check.GCSConnection == nil { 26 | return results.ErrorMessage(errors.New("missing GCS connection")) 27 | } 28 | 29 | var bucket string 30 | bucket, check.Path = parseGCSPath(check.Path) 31 | 32 | connection, err := ctx.HydrateConnectionByURL(check.GCPConnection.ConnectionName) 33 | if err != nil { 34 | return results.Failf("failed to populate GCS connection: %v", err) 35 | } else if connection == nil { 36 | connection = &models.Connection{Type: models.ConnectionTypeGCS} 37 | if check.GCSConnection.Bucket == "" { 38 | check.GCSConnection.Bucket = bucket 39 | } 40 | 41 | connection, err = connection.Merge(ctx, check.GCSConnection) 42 | if err != nil { 43 | return results.Failf("failed to populate GCS connection: %v", err) 44 | } 45 | } 46 | 47 | fs, err := artifacts.GetFSForConnection(ctx.Context, *connection) 48 | if err != nil { 49 | return results.ErrorMessage(err) 50 | } 51 | 52 | folders, err := genericFolderCheckWithoutPrecheck(fs, check.Path, check.Recursive, check.Filter) 53 | if err != nil { 54 | return results.ErrorMessage(err) 55 | } 56 | result.AddDetails(folders) 57 | 58 | if test := folders.Test(check.FolderTest); test != "" { 59 | return results.Failf(test) 60 | } 61 | 62 | return results 63 | } 64 | 65 | // parseGCSPath returns the bucket name and the actual path stripping of the gcs:// prefix and the bucket name. 66 | // The path is expected to be in the format "gcs://bucket_name/" 67 | func parseGCSPath(fullpath string) (bucket, path string) { 68 | trimmed := strings.TrimPrefix(fullpath, "gcs://") 69 | splits := strings.SplitN(trimmed, "/", 2) 70 | if len(splits) != 2 { 71 | return splits[0], "" 72 | } 73 | 74 | return splits[0], splits[1] 75 | } 76 | -------------------------------------------------------------------------------- /checks/aws_config.go: -------------------------------------------------------------------------------- 1 | //go:build !fast 2 | 3 | package checks 4 | 5 | import ( 6 | "github.com/aws/aws-sdk-go-v2/service/configservice" 7 | awsUtil "github.com/flanksource/artifacts/clients/aws" 8 | "github.com/flanksource/canary-checker/api/context" 9 | "github.com/flanksource/canary-checker/api/external" 10 | v1 "github.com/flanksource/canary-checker/api/v1" 11 | "github.com/flanksource/canary-checker/pkg" 12 | "github.com/flanksource/duty/connection" 13 | ) 14 | 15 | type AwsConfigChecker struct { 16 | } 17 | 18 | // Run: Check every entry from config according to Checker interface 19 | // Returns check result and metrics 20 | func (c *AwsConfigChecker) Run(ctx *context.Context) pkg.Results { 21 | var results pkg.Results 22 | for _, conf := range ctx.Canary.Spec.AwsConfig { 23 | results = append(results, c.Check(ctx, conf)...) 24 | } 25 | return results 26 | } 27 | 28 | // Type: returns checker type 29 | func (c *AwsConfigChecker) Type() string { 30 | return "awsconfig" 31 | } 32 | 33 | func (c *AwsConfigChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 34 | check := extConfig.(v1.AwsConfigCheck) 35 | result := pkg.Success(check, ctx.Canary) 36 | var results pkg.Results 37 | results = append(results, result) 38 | 39 | if check.AWSConnection == nil { 40 | check.AWSConnection = &connection.AWSConnection{} 41 | } else { 42 | if err := check.AWSConnection.Populate(ctx); err != nil { 43 | return results.Failf("failed to populate aws connection: %v", err) 44 | } 45 | } 46 | 47 | cfg, err := awsUtil.NewSession(ctx.Context, *check.AWSConnection) 48 | if err != nil { 49 | return results.ErrorMessage(err) 50 | } 51 | 52 | client := configservice.NewFromConfig(*cfg) 53 | if check.AggregatorName != nil { 54 | output, err := client.SelectAggregateResourceConfig(ctx, &configservice.SelectAggregateResourceConfigInput{ 55 | ConfigurationAggregatorName: check.AggregatorName, 56 | Expression: &check.Query, 57 | }) 58 | if err != nil { 59 | return results.ErrorMessage(err) 60 | } 61 | result.AddDetails(output.Results) 62 | } else { 63 | output, err := client.SelectResourceConfig(ctx, &configservice.SelectResourceConfigInput{ 64 | Expression: &check.Query, 65 | }) 66 | if err != nil { 67 | return results.ErrorMessage(err) 68 | } 69 | result.AddDetails(output.Results) 70 | } 71 | 72 | return results 73 | } 74 | -------------------------------------------------------------------------------- /checks/folder_s3.go: -------------------------------------------------------------------------------- 1 | //go:build !fast 2 | 3 | package checks 4 | 5 | import ( 6 | "errors" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go-v2/service/s3" 10 | "github.com/flanksource/artifacts" 11 | "github.com/flanksource/canary-checker/api/context" 12 | v1 "github.com/flanksource/canary-checker/api/v1" 13 | "github.com/flanksource/canary-checker/pkg" 14 | "github.com/flanksource/duty/models" 15 | ) 16 | 17 | type S3 struct { 18 | *s3.Client 19 | Bucket string 20 | } 21 | 22 | func CheckS3Bucket(ctx *context.Context, check v1.FolderCheck) pkg.Results { 23 | result := pkg.Success(check, ctx.Canary) 24 | var results pkg.Results 25 | results = append(results, result) 26 | 27 | if check.S3Connection == nil { 28 | return results.ErrorMessage(errors.New("missing AWS connection")) 29 | } 30 | 31 | var bucket string 32 | bucket, check.Path = parseS3Path(check.Path) 33 | 34 | connection, err := ctx.HydrateConnectionByURL(check.AWSConnection.ConnectionName) 35 | if err != nil { 36 | return results.Failf("failed to populate AWS connection: %v", err) 37 | } else if connection == nil { 38 | connection = &models.Connection{Type: models.ConnectionTypeS3} 39 | if check.S3Connection.Bucket == "" { 40 | check.S3Connection.Bucket = bucket 41 | } 42 | 43 | connection, err = connection.Merge(ctx, check.S3Connection) 44 | if err != nil { 45 | return results.Failf("failed to populate AWS connection: %v", err) 46 | } 47 | } 48 | 49 | fs, err := artifacts.GetFSForConnection(ctx.Context, *connection) 50 | if err != nil { 51 | return results.ErrorMessage(err) 52 | } 53 | 54 | folders, err := genericFolderCheckWithoutPrecheck(fs, check.Path, check.Recursive, check.Filter) 55 | if err != nil { 56 | return results.ErrorMessage(err) 57 | } 58 | result.AddDetails(folders) 59 | 60 | if test := folders.Test(check.FolderTest); test != "" { 61 | return results.Failf(test) 62 | } 63 | 64 | return results 65 | } 66 | 67 | // parseS3Path returns the bucket name and the actual path stripping of the s3:// prefix and the bucket name. 68 | // The path is expected to be in the format "s3://bucket_name/" 69 | func parseS3Path(fullpath string) (bucket, path string) { 70 | trimmed := strings.TrimPrefix(fullpath, "s3://") 71 | splits := strings.SplitN(trimmed, "/", 2) 72 | if len(splits) != 2 { 73 | return splits[0], "" 74 | } 75 | 76 | return splits[0], splits[1] 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | pull_request: 8 | merge_group: 9 | permissions: read-all 10 | jobs: 11 | golangci: 12 | permissions: 13 | contents: read # for actions/checkout to fetch code 14 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 19 | - name: Install Go 20 | uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 21 | with: 22 | go-version: 1.22.x 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 25 | with: 26 | # Disable caching as a workaround for https://github.com/golangci/golangci-lint-action/issues/135. 27 | # The line can be removed once the golangci-lint issue is resolved. version: v1.55.2 28 | skip-pkg-cache: true 29 | 30 | - name: setup node 31 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 32 | with: 33 | node-version: "12" 34 | - name: Check auto-generated files 35 | env: 36 | CI: false 37 | run: | 38 | make resources 39 | git checkout hack/generate-schemas/go.* 40 | git checkout fixtures/datasources/go.* 41 | git diff 42 | changed_files=$(git status -s) 43 | [[ -z "$changed_files" ]] || (printf "Change is detected in some files: \n$changed_files\n Did you run 'make resources' before sending the PR?" && exit 1) 44 | helm: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 48 | - name: Set up Helm 49 | uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 50 | with: 51 | version: v3.4.0 52 | - name: Set up Python 53 | uses: actions/setup-python@e9aba2c848f5ebd159c070c61ea2c4e2b122355e # v2.3.4 54 | with: 55 | python-version: 3.7 56 | - name: Set up chart-testing 57 | uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 58 | - name: Lint chart 59 | run: ct lint --charts ./chart 60 | -------------------------------------------------------------------------------- /pkg/api/suite_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | apiContext "github.com/flanksource/canary-checker/api/context" 9 | "github.com/flanksource/canary-checker/pkg/cache" 10 | "github.com/flanksource/canary-checker/pkg/echo" 11 | "github.com/flanksource/canary-checker/pkg/utils" 12 | "github.com/flanksource/commons/logger" 13 | "github.com/flanksource/duty/context" 14 | "github.com/flanksource/duty/tests/setup" 15 | echov4 "github.com/labstack/echo/v4" 16 | "github.com/onsi/ginkgo/v2" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var ( 21 | testEchoServer *echov4.Echo 22 | testEchoServerPort int 23 | ctx context.Context 24 | httpCheckCallCounter int 25 | ) 26 | 27 | func TestAPI(t *testing.T) { 28 | RegisterFailHandler(ginkgo.Fail) 29 | ginkgo.RunSpecs(t, "API") 30 | } 31 | 32 | var _ = ginkgo.BeforeSuite(func() { 33 | 34 | ctx = setup.BeforeSuiteFn().WithDBLogLevel("trace").WithTrace() 35 | apiContext.DefaultContext = ctx 36 | testEchoServer = echo.New(ctx) 37 | cache.PostgresCache = cache.NewPostgresCache(ctx) 38 | 39 | // A dummy endpoint used by the HTTP check 40 | testEchoServer.GET("/http-check", func(c echov4.Context) error { 41 | httpCheckCallCounter++ 42 | resp := map[string][]map[string]string{ 43 | "alerts": { 44 | { 45 | "name": "http-check", 46 | "icon": "http", 47 | "message": "A dummy http check", 48 | "description": "A dummy http check", 49 | }, 50 | }, 51 | } 52 | 53 | if httpCheckCallCounter > 1 { 54 | resp["alerts"][0]["deleted_at"] = "2023-10-30T09:00:00Z" 55 | } 56 | 57 | return c.JSON(http.StatusOK, resp) 58 | }) 59 | 60 | testEchoServerPort = utils.FreePort() 61 | listenAddr := fmt.Sprintf(":%d", testEchoServerPort) 62 | 63 | go func() { 64 | defer ginkgo.GinkgoRecover() // Required by ginkgo, if an assertion is made in a goroutine. 65 | if err := testEchoServer.Start(listenAddr); err != nil { 66 | if err == http.ErrServerClosed { 67 | logger.Infof("Server closed") 68 | } else { 69 | ginkgo.Fail(fmt.Sprintf("Failed to start test server: %v", err)) 70 | } 71 | } 72 | }() 73 | }) 74 | 75 | var _ = ginkgo.AfterSuite(func() { 76 | logger.Infof("Stopping test echo server") 77 | if err := testEchoServer.Close(); err != nil { 78 | ginkgo.Fail(err.Error()) 79 | } 80 | setup.AfterSuiteFn() 81 | }) 82 | -------------------------------------------------------------------------------- /pkg/controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Kubernetes authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo/v2" 24 | . "github.com/onsi/gomega" 25 | "k8s.io/client-go/kubernetes/scheme" 26 | "k8s.io/client-go/rest" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "sigs.k8s.io/controller-runtime/pkg/envtest" 29 | logf "sigs.k8s.io/controller-runtime/pkg/log" 30 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 31 | 32 | canariesv1 "github.com/flanksource/canary-checker/api/v1" 33 | // +kubebuilder:scaffold:imports 34 | ) 35 | 36 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 37 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 38 | 39 | var cfg *rest.Config 40 | var k8sClient client.Client 41 | var testEnv *envtest.Environment 42 | 43 | func TestAPIs(t *testing.T) { 44 | RegisterFailHandler(Fail) 45 | RunSpecs(t, "Controller Suite") 46 | } 47 | 48 | var _ = BeforeSuite(func() { 49 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 50 | 51 | By("bootstrapping test environment") 52 | testEnv = &envtest.Environment{ 53 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 54 | } 55 | 56 | var err error 57 | cfg, err = testEnv.Start() 58 | Expect(err).ToNot(HaveOccurred()) 59 | Expect(cfg).ToNot(BeNil()) 60 | 61 | err = canariesv1.AddToScheme(scheme.Scheme) 62 | Expect(err).NotTo(HaveOccurred()) 63 | 64 | // +kubebuilder:scaffold:scheme 65 | 66 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 67 | Expect(err).ToNot(HaveOccurred()) 68 | Expect(k8sClient).ToNot(BeNil()) 69 | }) 70 | 71 | var _ = AfterSuite(func() { 72 | By("tearing down the test environment") 73 | err := testEnv.Stop() 74 | Expect(err).ToNot(HaveOccurred()) 75 | }) 76 | --------------------------------------------------------------------------------