├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── aws-exec.yml │ ├── build.yml │ ├── codeql.yml │ ├── e2e-operator.yml │ ├── gotest.yml │ ├── helm-test.yml │ ├── lint.yml │ ├── release.yml │ ├── scorecard.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── .releaserc ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── Tiltfile ├── api ├── context │ ├── context.go │ └── functions.go ├── external │ ├── api.go │ ├── metrics.go │ └── zz_generated.deepcopy.go └── v1 │ ├── canary_types.go │ ├── checks.go │ ├── common.go │ ├── component_types.go │ ├── conditions.go │ ├── db_types.go │ ├── groupversion_info.go │ ├── system_types.go │ ├── varsource.go │ └── zz_generated.deepcopy.go ├── build ├── dev │ └── Dockerfile ├── full │ └── Dockerfile ├── minimal │ └── Dockerfile └── slim │ └── Dockerfile ├── canary-checker.properties ├── chart ├── .gitignore ├── .helmignore ├── Chart.yaml ├── Makefile ├── README.md ├── README.md.tpl ├── ci │ ├── full-values.yaml │ └── namespaced.yaml ├── crds │ ├── Canary.yml │ ├── Component.yml │ └── Topology.yml ├── dashboards │ ├── Details.json │ └── Overview.json ├── deref.ts ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── grafanaDashboard.yaml │ ├── ingress.yaml │ ├── postgres-secret.yaml │ ├── postgres-service.yaml │ ├── postgres-statefulset.yaml │ ├── rbac.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── servicemonitor.yaml ├── test.sh ├── values.schema.deref.json ├── values.schema.json └── values.yaml ├── checks ├── alertmanager.go ├── aws_config.go ├── aws_config_rule.go ├── aws_stubs.go ├── azure_devops.go ├── azure_devops_test.go ├── catalog.go ├── checker.go ├── cloudwatch.go ├── common.go ├── database_backup.go ├── database_backup_gcp.go ├── dns.go ├── dynatrace.go ├── elasticsearch.go ├── exec.go ├── folder.go ├── folder_check.go ├── folder_gcs.go ├── folder_s3.go ├── folder_s3_test.go ├── folder_sftp.go ├── folder_smb.go ├── folder_test.go ├── git_protocol.go ├── github.go ├── http.go ├── icmp.go ├── jmeter.go ├── junit.go ├── junit_xml.go ├── kubernetes.go ├── kubernetes_resource.go ├── ldap.go ├── metrics.go ├── mongodb.go ├── mssql.go ├── mysql.go ├── namespace.go ├── opensearch.go ├── pod.go ├── postgres.go ├── prometheus.go ├── pubsub.go ├── redis.go ├── restic.go ├── runchecks.go ├── s3.go ├── sql.go ├── stubs_windows.go ├── tcp.go ├── timer.go ├── utils.go └── webhook.go ├── cmd ├── docs.go ├── offline.go ├── operator.go ├── output │ ├── csv.go │ ├── junit.go │ └── output.go ├── push.go ├── root.go ├── run.go ├── serve.go ├── sync.go └── topology.go ├── config ├── base │ ├── ingress.yaml │ ├── kustomization.yaml │ ├── manager.yaml │ └── rbac.yaml ├── canary-dev.yaml ├── deploy │ ├── Canary.yml │ ├── Component.yml │ ├── Topology.yml │ ├── base.yaml │ └── manifests.yaml ├── kustomization.yaml ├── namespace.yaml └── schemas │ ├── canary.schema.json │ ├── component.schema.json │ ├── health_alertmanager.schema.json │ ├── health_awsconfig.schema.json │ ├── health_awsconfigrule.schema.json │ ├── health_azuredevops.schema.json │ ├── health_catalog.schema.json │ ├── health_cloudwatch.schema.json │ ├── health_configdb.schema.json │ ├── health_containerdPull.schema.json │ ├── health_containerdPush.schema.json │ ├── health_databasebackupcheck.schema.json │ ├── health_dns.schema.json │ ├── health_dockerPull.schema.json │ ├── health_dockerPush.schema.json │ ├── health_dynatrace.schema.json │ ├── health_ec2.schema.json │ ├── health_elasticsearch.schema.json │ ├── health_exec.schema.json │ ├── health_folder.schema.json │ ├── health_gcp_incidents.schema.json │ ├── health_gitProtocol.schema.json │ ├── health_github.schema.json │ ├── health_helm.schema.json │ ├── health_http.schema.json │ ├── health_icmp.schema.json │ ├── health_jmeter.schema.json │ ├── health_junit.schema.json │ ├── health_kubernetes.schema.json │ ├── health_ldap.schema.json │ ├── health_mongodb.schema.json │ ├── health_mssql.schema.json │ ├── health_mysql.schema.json │ ├── health_namespace.schema.json │ ├── health_opensearch.schema.json │ ├── health_pod.schema.json │ ├── health_postgres.schema.json │ ├── health_prometheus.schema.json │ ├── health_pubsub.schema.json │ ├── health_redis.schema.json │ ├── health_restic.schema.json │ ├── health_s3.schema.json │ ├── health_tcp.schema.json │ ├── health_webhook.schema.json │ └── topology.schema.json ├── fixtures-crd └── k8s │ └── _setup.yaml ├── fixtures ├── _setup.yaml ├── aws │ ├── aws_config_pass.yaml │ ├── aws_config_rule_pass.yaml │ ├── cloudwatch_pass.yaml │ ├── kustomization.yaml │ ├── minimal │ │ ├── _setup.sh │ │ └── aws_exec_pass.yaml │ ├── s3-protocol.yaml │ └── s3_bucket_pass.yaml ├── azure │ └── devops.yaml ├── datasources │ ├── GCP │ │ ├── database_backup.yaml │ │ ├── database_backup_cred.yaml │ │ ├── database_backup_cred_from_connection.yaml │ │ └── folder_pass.yaml │ ├── SFTP │ │ ├── sftp_fail_connection.yaml │ │ └── sftp_pass.yaml │ ├── _karina.yaml │ ├── _post_setup.sh │ ├── _setup.yaml │ ├── alertmanager.yaml │ ├── alertmanager_mix.yaml │ ├── folder_fail.yaml │ ├── folder_pass.yaml │ ├── folder_single_pass.yaml │ ├── folder_with_filter.yaml │ ├── go.mod │ ├── go.sum │ ├── kustomization.yaml │ ├── main.go │ ├── mongo_fail.yaml │ ├── mongo_pass.yaml │ ├── mssql_fail.yaml │ ├── mssql_pass.yaml │ ├── mysql_fail.yaml │ ├── mysql_pass.yaml │ ├── posgres_stateful_pass.yaml │ ├── postgres.yaml │ ├── postgres_empty_result_fail.yaml │ ├── postgres_empty_result_pass.yaml │ ├── postgres_fail.yaml │ ├── postgres_pass.yaml │ ├── prometheus.yaml │ ├── redis_fail.yaml │ ├── redis_pass.yaml │ ├── s3_bucket_fail.yaml │ └── s3_bucket_pass.yaml ├── elasticsearch │ ├── _post_setup.sh │ ├── _setup.yaml │ ├── elasticsearch_fail.yaml │ ├── elasticsearch_pass.yaml │ ├── kustomization.yaml │ └── stateful_metrics.yaml ├── external │ ├── alertmanager.yaml │ ├── catalog.yaml │ ├── crossplane-kubernetes-resource.yaml │ ├── dynatrace.yaml │ ├── github-webhook.yaml │ └── pubsub-gcp.yaml ├── git │ ├── _image │ ├── _setup.sh │ ├── exec_checkout_pass.yaml │ ├── git_check_pass.yaml │ ├── git_pull_push_pass.yaml │ ├── git_test_expression_pass.yaml │ ├── gitea.values │ └── kustomization.yaml ├── k8s │ ├── _karina.yaml │ ├── _setup.sh │ ├── _setup.yaml │ ├── certmanager.yaml │ ├── cronjob_monitor.yaml │ ├── cronjob_monitor_fail.yaml │ ├── helm_check.yaml │ ├── http_auth_configmap.yaml │ ├── http_auth_sa.yaml │ ├── http_auth_secret.yaml │ ├── junit_fail.yaml │ ├── junit_pass.yaml │ ├── junit_pass_metrics.yaml │ ├── kubernetes-check-inline-kubeconfig.yaml │ ├── kubernetes-check-kubeconfig-from-file.yaml │ ├── kubernetes-check-kubeconfig-from-secrets.yaml │ ├── kubernetes-minimal_pass.yaml │ ├── kubernetes-resource-check-inline-kubeconfig.yaml │ ├── kubernetes-resource-check-kubeconfig-from-file.yaml │ ├── kubernetes-resource-check-kubeconfig-from-secrets.yaml │ ├── kubernetes_bundle.yaml │ ├── kubernetes_pass.yaml │ ├── kubernetes_resource_ingress_pass.yaml │ ├── kubernetes_resource_namespace_pass.yaml │ ├── kubernetes_resource_pod_exit_code_pass.yaml │ ├── kubernetes_resource_postgresql_helmrelease.yaml │ ├── kubernetes_resource_service_fail.yaml │ ├── kubernetes_resource_service_pass.yaml │ ├── kustomization.yaml │ ├── pod_fail.yaml │ ├── pod_pass.yaml │ ├── secret_sanitize_fail.yaml │ ├── slow │ │ └── namespace_pass.yaml │ └── vcluster-canary-checker.yaml ├── kustomization.yaml ├── ldap │ ├── _setup.yaml │ ├── kustomization.yaml │ └── ldap_pass.yaml ├── minimal │ ├── cel.yaml │ ├── containerd-pull.yaml │ ├── containerd-push.yaml │ ├── display-with-cel_pass.yaml │ ├── display-with-gotemplate_pass.yaml │ ├── display-with-javascript_pass.yaml │ ├── dns_fail.yaml │ ├── dns_pass.yaml │ ├── exec_artifact.yaml │ ├── exec_connection_aws_fail.yaml │ ├── exec_env_pass.yaml │ ├── exec_fail.yaml │ ├── exec_pass.yaml │ ├── http-check-labels.yaml │ ├── http-crawl_pass.yaml │ ├── http-templateBody.yaml │ ├── http_auth_from_config_map.yaml │ ├── http_auth_from_helm_ref.yaml │ ├── http_auth_from_secret.yaml │ ├── http_auth_from_service_account.yaml │ ├── http_auth_static_pass.yaml │ ├── http_auth_url_pass.yaml │ ├── http_fail.yaml │ ├── http_fail_connection.yaml │ ├── http_no_auth_pass.yaml │ ├── http_pass.yaml │ ├── http_pass_results_mode_pass.yaml │ ├── http_simple.yaml │ ├── http_single_pass.yaml │ ├── http_template.yaml │ ├── http_timeout_fail.yaml │ ├── http_tls_check_pass.yaml │ ├── http_tls_config.yaml │ ├── http_trace_pass.yaml │ ├── icmp_fail.yaml │ ├── jmeter.yaml │ ├── kustomization.yaml │ ├── metrics-multiple.yaml │ ├── metrics-transformed.yaml │ ├── metrics.yaml │ ├── namespaced_check_pass.yaml │ ├── schedule_invalid.yaml │ ├── tcp.yaml │ └── tcp_invalid.yaml ├── namespace.yml ├── opensearch │ ├── _post_setup.sh │ ├── _setup.yaml │ ├── kustomization.yaml │ ├── opensearch_fail.yaml │ └── opensearch_pass.yaml ├── prometheus │ ├── jobs-fail-only.yaml │ └── jobs.yaml ├── quarantine │ ├── icmp_pass.yaml │ ├── s3_fail.yaml │ └── smb_pass.yaml ├── restic │ ├── _image │ ├── _karina.yaml │ ├── _setup.sh │ ├── restic_fail.yaml │ ├── restic_with_integrity_pass.yaml │ └── restic_without_integrity_pass.yaml └── topology │ ├── canary-selector.yaml │ ├── component-status-expr.yml │ ├── component-with-for-each.yml │ ├── component-with-parent-lookup.yml │ ├── component-with-properties.yml │ ├── component-with-property-list.yml │ ├── inline-check.yaml │ ├── k8s-system.yaml │ ├── kube-dns.yaml │ ├── kubernetes-cluster-group-by.yaml │ ├── kubernetes-lookup-inline-configmap.yaml │ ├── kubernetes-lookup-kubeconfig-from-file.yaml │ ├── kubernetes-lookup-kubeconfig-from-secrets.yaml │ ├── kubernetes-lookup.yaml │ ├── kustomization.yaml │ ├── selector.yaml │ └── single-check.yaml ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── compress-crds.sh └── generate-schemas │ ├── .gitignore │ └── main.go ├── install-service.ps1 ├── main.go ├── osv-scanner.toml ├── pkg ├── api.go ├── api │ ├── api.go │ ├── api_test.go │ ├── details.go │ ├── push.go │ ├── run_now.go │ ├── suite_test.go │ ├── utils.go │ ├── webhook.go │ └── webhook_test.go ├── cache │ ├── postgres.go │ └── postgres_util.go ├── config.go ├── controllers │ ├── canary_controller.go │ ├── suite_test.go │ └── system_controller.go ├── db │ ├── canary.go │ ├── check_statuses.go │ └── topology.go ├── dns │ └── dns.go ├── echo │ └── server.go ├── jobs │ ├── canary │ │ ├── canary_jobs.go │ │ ├── canary_jobs_test.go │ │ ├── relationships.go │ │ ├── status.go │ │ ├── suite_test.go │ │ ├── sync.go │ │ └── sync_upstream.go │ ├── jobs.go │ └── topology │ │ └── topology_jobs.go ├── labels │ └── labels.go ├── metrics │ └── metrics.go ├── prometheus │ └── prometheus.go ├── results.go ├── runner │ ├── runner.go │ └── runner_test.go ├── sync │ ├── sync.go │ └── topology.go ├── system_api.go ├── telemetry │ └── tracer.go ├── topology │ ├── cleanup.go │ ├── component_check.go │ ├── component_check_test.go │ ├── component_config.go │ ├── component_config_test.go │ ├── component_relationship.go │ ├── component_relationship_test.go │ ├── context.go │ ├── jobs.go │ ├── run.go │ ├── run_test.go │ ├── suite_test.go │ └── utils.go └── utils │ ├── utils.go │ └── utils_test.go └── test ├── Makefile ├── aggregate-test ├── Procfile └── config │ ├── main.yaml │ ├── server1.yaml │ └── server2.yaml ├── e2e-operator.sh ├── e2e.sh ├── karina.yaml ├── nested-canaries └── ec2-http.yaml ├── patch1.yaml └── run_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .idea/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | chart/ 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | groups: 9 | go-modules: 10 | patterns: 11 | - "*" # All go.mod dependencies 12 | 13 | - package-ecosystem: github-actions 14 | directory: / 15 | schedule: 16 | interval: daily 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | 22 | 23 | - package-ecosystem: docker 24 | directories: 25 | - /build/dev 26 | - /build/minimal 27 | - /build/full 28 | - /build/slim 29 | schedule: 30 | interval: daily 31 | groups: 32 | docker-updates: 33 | patterns: 34 | - "*" 35 | -------------------------------------------------------------------------------- /.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@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 22 | with: 23 | go-version: 1.25.x 24 | - name: Checkout code 25 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 26 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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@00943011d9042930efac3dcd3a170e4273319bc8 # v5.1.0 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@3a74b2957438d0b6e2e61d67b05318aa25c9e6c6 # v2.20.0 50 | if: always() && github.event.repository.fork == 'false' 51 | with: 52 | files: test/test-results.xml 53 | check_name: E2E - ${{matrix.suite}} 54 | -------------------------------------------------------------------------------- /.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 | - linux compress 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 18 | - name: Build Container 19 | run: make ${{matrix.target}} 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.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@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 29 | with: 30 | go-version: 1.25.x 31 | 32 | - name: Checkout code 33 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 34 | 35 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.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 | -------------------------------------------------------------------------------- /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | permissions: 4 | contents: read 5 | checks: read 6 | issues: read 7 | pull-requests: read 8 | name: Go Test 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Go 14 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 15 | with: 16 | go-version: 1.25.x 17 | - name: Checkout code 18 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 19 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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@3a74b2957438d0b6e2e61d67b05318aa25c9e6c6 # v2.20.0 31 | if: always() && github.event.repository.fork == 'false' 32 | with: 33 | files: test/test-results.xml 34 | check_name: E2E - ${{matrix.suite}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bin/ 2 | bin/ 3 | .creds/ 4 | .release/ 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 | .windsurfrules -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | tests: false 4 | linters: 5 | default: none 6 | enable: 7 | - bodyclose 8 | - dogsled 9 | - errcheck 10 | - goconst 11 | - goprintffuncname 12 | - govet 13 | - ineffassign 14 | - misspell 15 | - nakedret 16 | - nosprintfhostport 17 | - reassign 18 | - rowserrcheck 19 | - staticcheck 20 | - unconvert 21 | - unparam 22 | - unused 23 | - whitespace 24 | settings: 25 | govet: 26 | disable: 27 | - printf 28 | exclusions: 29 | generated: lax 30 | presets: 31 | - comments 32 | - common-false-positives 33 | - legacy 34 | - std-error-handling 35 | paths: 36 | - third_party$ 37 | - builtin$ 38 | - examples$ 39 | rules: 40 | - linters: 41 | - staticcheck 42 | text: 'QF1008:' 43 | formatters: 44 | enable: 45 | - gofmt 46 | - goimports 47 | exclusions: 48 | generated: lax 49 | paths: 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /api/external/api.go: -------------------------------------------------------------------------------- 1 | package external 2 | 3 | import "github.com/google/uuid" 4 | 5 | type Endpointer interface { 6 | GetEndpoint() string 7 | } 8 | 9 | type Describable interface { 10 | GetDescription() string 11 | GetIcon() string 12 | GetName() string 13 | GetNamespace() string 14 | GetLabels() map[string]string 15 | GetTransformDeleteStrategy() string 16 | GetMetricsSpec() []Metrics 17 | GetCustomUUID() uuid.UUID 18 | GetHash() string 19 | ShouldMarkFailOnEmpty() bool 20 | } 21 | 22 | type WithType interface { 23 | GetType() string 24 | } 25 | 26 | type Check interface { 27 | Endpointer 28 | Describable 29 | WithType 30 | } 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build/dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04@sha256:bb1c41682308d7040f74d103022816d41c50d7b0c89e9d706a74b4e548636e54 2 | WORKDIR /app 3 | 4 | # Install restic from releases 5 | RUN apt-get update && \ 6 | apt-get install -y curl && \ 7 | curl -L https://github.com/restic/restic/releases/download/v0.12.0/restic_0.12.0_linux_amd64.bz2 -o restic.bz2 && \ 8 | bunzip2 /app/restic.bz2 && \ 9 | chmod +x /app/restic && \ 10 | mv /app/restic /usr/local/bin/ && \ 11 | rm -rf /app/restic.bz2 12 | 13 | #Install jmeter 14 | RUN curl -L https://mirrors.estointernet.in/apache//jmeter/binaries/apache-jmeter-5.4.1.tgz -o apache-jmeter-5.4.1.tgz && \ 15 | tar xf apache-jmeter-5.4.1.tgz -C / && \ 16 | rm /app/apache-jmeter-5.4.1.tgz && \ 17 | apt-get install -y openjdk-11-jre-headless 18 | 19 | ENV PATH /apache-jmeter-5.4.1/bin/:$PATH 20 | 21 | RUN apt-get update && \ 22 | apt-get install -y ca-certificates && \ 23 | rm -Rf /var/lib/apt/lists/* && \ 24 | rm -Rf /usr/share/doc && rm -Rf /usr/share/man && \ 25 | apt-get clean 26 | COPY ./.bin/canary-checker /app/canary-checker 27 | ENTRYPOINT ["/app/canary-checker"] 28 | -------------------------------------------------------------------------------- /build/full/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25-bookworm@sha256:ee420c17fa013f71eca6b35c3547b854c838d4f26056a34eb6171bba5bf8ece4 AS builder 2 | WORKDIR /app 3 | 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | ARG NAME 8 | ARG VERSION 9 | ENV IMAGE_TYPE=full 10 | 11 | COPY go.mod /app/go.mod 12 | COPY go.sum /app/go.sum 13 | RUN go mod download 14 | 15 | COPY ./ ./ 16 | RUN OS=${TARGETOS} ARCH=${TARGETARCH} make build 17 | 18 | FROM flanksource/base-image-canary-checker:0.5.18@sha256:034e86687f230c4c8056aa6e1924ba3eac698de0968edebbebc56844a18cf61c 19 | ARG TARGETARCH 20 | 21 | WORKDIR /app 22 | 23 | RUN mkdir /opt/database && groupadd --gid 1000 canary && \ 24 | useradd canary --uid 1000 -g canary -m -d /var/lib/canary && \ 25 | chown -R 1000:1000 /opt/database && chown -R 1000:1000 /app 26 | 27 | USER canary:canary 28 | 29 | ENV PATH="${PATH}:/var/lib/canary/bin/" 30 | 31 | COPY --from=builder /app/.bin/canary-checker /app 32 | 33 | RUN /app/canary-checker go-offline 34 | 35 | ENTRYPOINT ["/app/canary-checker"] 36 | -------------------------------------------------------------------------------- /build/minimal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25-bookworm@sha256:ee420c17fa013f71eca6b35c3547b854c838d4f26056a34eb6171bba5bf8ece4 AS builder 2 | WORKDIR /app 3 | 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | ARG NAME 8 | ARG VERSION 9 | ENV IMAGE_TYPE=minimal 10 | 11 | COPY go.mod /app/go.mod 12 | COPY go.sum /app/go.sum 13 | RUN go mod download 14 | 15 | COPY ./ ./ 16 | RUN OS=${TARGETOS} ARCH=${TARGETARCH} make build 17 | 18 | FROM flanksource/base-image:0.5.18@sha256:df7881a6a3b944dca86e0b0485c355b2103ea62e1d218a2476bbdf1240ea8908 19 | ARG TARGETARCH 20 | 21 | WORKDIR /app 22 | 23 | RUN mkdir /opt/database && groupadd --gid 1000 canary && \ 24 | useradd canary --uid 1000 -g canary -m -d /var/lib/canary && \ 25 | chown -R 1000:1000 /opt/database && chown -R 1000:1000 /app 26 | 27 | USER canary:canary 28 | 29 | COPY --from=builder /app/.bin/canary-checker /app 30 | 31 | RUN /app/canary-checker go-offline 32 | 33 | ENTRYPOINT ["/app/canary-checker"] 34 | -------------------------------------------------------------------------------- /build/slim/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25-bookworm@sha256:ee420c17fa013f71eca6b35c3547b854c838d4f26056a34eb6171bba5bf8ece4 AS builder 2 | WORKDIR /app 3 | 4 | ARG TARGETOS 5 | ARG TARGETARCH 6 | 7 | ARG NAME 8 | ARG VERSION 9 | ENV IMAGE_TYPE=slim 10 | 11 | COPY go.mod /app/go.mod 12 | COPY go.sum /app/go.sum 13 | RUN go mod download 14 | 15 | COPY ./ ./ 16 | RUN OS=${TARGETOS} ARCH=${TARGETARCH} make build 17 | 18 | FROM debian:bookworm-slim@sha256:12c396bd585df7ec21d5679bb6a83d4878bc4415ce926c9e5ea6426d23c60bdc AS base 19 | WORKDIR /app 20 | ARG TARGETARCH 21 | ENV DEBIAN_FRONTEND=noninteractive 22 | ENV LC_ALL=en_US.UTF-8 23 | ENV LANG=en_US.UTF-8 24 | 25 | RUN apt-get update && \ 26 | apt-get install -y \ 27 | curl wget less \ 28 | unzip zip wget gnupg2 bzip2 \ 29 | locales locales-all tzdata \ 30 | apt-transport-https ca-certificates lsb-release git python3-crcmod python3-openssl \ 31 | jq yq \ 32 | --no-install-recommends && \ 33 | locale-gen en_US.UTF-8 && \ 34 | update-locale LANG=en_US.UTF-8 && \ 35 | rm -Rf /var/lib/apt/lists/* && \ 36 | apt-get clean 37 | 38 | RUN apt-get update && apt-get upgrade -y && \ 39 | rm -Rf /var/lib/apt/lists/* && \ 40 | apt-get clean 41 | 42 | RUN mkdir /opt/database && groupadd --gid 1000 canary && \ 43 | useradd canary --uid 1000 -g canary -m -d /var/lib/canary && \ 44 | chown -R 1000:1000 /opt/database && chown -R 1000:1000 /app 45 | 46 | USER canary:canary 47 | 48 | COPY --from=builder /app/.bin/canary-checker /app 49 | 50 | RUN /app/canary-checker go-offline 51 | 52 | ENTRYPOINT ["/app/canary-checker"] 53 | -------------------------------------------------------------------------------- /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 | log.level.db=warn 9 | 10 | # jobs.ComponentRelationshipSync.runNow=true 11 | -------------------------------------------------------------------------------- /chart/.gitignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.772" 14 | repository: https://flanksource.github.io/charts 15 | condition: flanksource-ui.enabled 16 | -------------------------------------------------------------------------------- /chart/Makefile: -------------------------------------------------------------------------------- 1 | 2 | LOCALBIN ?= $(shell pwd)/.bin 3 | 4 | $(LOCALBIN): 5 | mkdir -p .bin 6 | 7 | .PHONY: values.schema.json 8 | values.schema.json: .bin/helm-schema 9 | .bin/helm-schema -r -f values.yaml 10 | # remove empty required arrays 11 | jq 'walk(if type == "object" and has("required") and (.required | type == "array" and length == 0) then del(.required) else . end)' values.schema.json > tmp.json && mv tmp.json values.schema.json 12 | # trim spaces 13 | jq 'walk(if type == "object" and .description? then .description |= gsub("^(\\s+)|(\\s+)$$"; "") else . end )' values.schema.json > tmp.json && mv tmp.json values.schema.json 14 | 15 | 16 | .PHONY: chart 17 | chart: values.schema.deref.json README.md 18 | 19 | .PHONY: README.md 20 | README.md: .bin/helm-docs 21 | .bin/helm-docs -t README.md.tpl --skip-version-footer 22 | 23 | 24 | EGET=$(LOCALBIN)/eget 25 | EGET_BIN=$(LOCALBIN) 26 | $(EGET): $(LOCALBIN) 27 | GOBIN=$(LOCALBIN) go install github.com/zyedidia/eget@v1.3.4 28 | 29 | .bin/helm-docs: $(EGET) 30 | $(EGET) -t v1.14.2 norwoodj/helm-docs --upgrade-only --to $(EGET_BIN) 31 | 32 | .bin/helm-schema: $(EGET) 33 | $(EGET) -t 0.18.1 dadav/helm-schema --upgrade-only --to $(EGET_BIN) 34 | 35 | .phony: values.schema.deref.json 36 | values.schema.deref.json: values.schema.json 37 | npm i @apidevtools/json-schema-ref-parser 38 | npx @digitak/esrun deref.ts 39 | 40 | 41 | .PHONY: lint 42 | lint: chart 43 | ct lint --charts . 44 | -------------------------------------------------------------------------------- /chart/README.md.tpl: -------------------------------------------------------------------------------- 1 | {{ template "chart.header" . }} 2 | {{ template "chart.deprecationWarning" . }} 3 | 4 | {{ template "chart.description" . }} 5 | 6 | {{ template "chart.homepageLine" . }} 7 | 8 | {{ template "chart.sourcesSection" . }} 9 | 10 | {{ template "chart.requirementsSection" . }} 11 | 12 | {{ template "chart.valuesSection" . }} 13 | 14 | {{ template "chart.maintainersSection" . }} 15 | -------------------------------------------------------------------------------- /chart/ci/namespaced.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=../values.schema.json 2 | 3 | canaryNamespace: default 4 | serviceAccount: 5 | rbac: 6 | clusterRole: false 7 | -------------------------------------------------------------------------------- /chart/crds/Canary.yml: -------------------------------------------------------------------------------- 1 | ../../config/deploy/Canary.yml -------------------------------------------------------------------------------- /chart/crds/Component.yml: -------------------------------------------------------------------------------- 1 | ../../config/deploy/Component.yml -------------------------------------------------------------------------------- /chart/crds/Topology.yml: -------------------------------------------------------------------------------- 1 | ../../config/deploy/Topology.yml -------------------------------------------------------------------------------- /chart/deref.ts: -------------------------------------------------------------------------------- 1 | import $RefParser from "@apidevtools/json-schema-ref-parser"; 2 | import fs from "fs"; 3 | 4 | 5 | let schema = JSON.parse(fs.readFileSync('values.schema.json', 'utf8')); 6 | await $RefParser.dereference(schema) 7 | 8 | fs.writeFileSync('values.schema.deref.json', JSON.stringify(schema, null, 2)); 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ) | b64dec ) | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | {{- range $k, $v := .Values.serviceMonitorLabels }} 9 | {{ $k }}: {{ $v | quote }} 10 | {{- end }} 11 | spec: 12 | jobLabel: {{ include "canary-checker.name" . }} 13 | endpoints: 14 | - port: http 15 | {{- if and .Values.serviceMonitorTlsConfig .Values.serviceMonitorTlsConfig.enabled }} 16 | scheme: https 17 | tlsConfig: 18 | {{- toYaml .Values.serviceMonitorTlsConfig | nindent 8 }} 19 | {{- end }} 20 | interval: 30s 21 | honorLabels: true 22 | metricRelabelings: 23 | - action: labeldrop 24 | regex: (pod|instance) 25 | selector: 26 | matchLabels: 27 | {{- include "canary-checker.selectorLabels" . | nindent 6 }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /chart/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ns="-n $1" 4 | 5 | kubectl apply -f fixtures/minimal/exec_pass.yaml $ns 6 | 7 | kubectl get pods --all-namespaces 8 | 9 | kubectl logs -n canary-checker deploy/canary-checker 10 | 11 | function get_unused_port() { 12 | for port in $(seq 4444 65000); 13 | do 14 | echo -ne "\035" | telnet 127.0.0.1 $port > /dev/null 2>&1; 15 | [ $? -eq 1 ] && echo "$port" && break; 16 | done 17 | } 18 | 19 | 20 | PORT=$(get_unused_port) 21 | kubectl port-forward $ns svc/canary-checker $PORT:8080 & 22 | PID=$! 23 | function cleanup { 24 | echo "Cleaning up..." 25 | kill $PID 26 | } 27 | 28 | trap cleanup EXIT 29 | 30 | sleep 60 31 | 32 | status=$(kubectl get $ns canaries.canaries.flanksource.com exec-pass -o yaml | yq .status.status) 33 | echo "Status=$status" 34 | if [[ $status != "Passed" ]]; then 35 | exit 1 36 | fi 37 | 38 | if ! curl -vv --fail "http://localhost:$PORT/health"; then 39 | echo "Call to health failed" 40 | exit 1 41 | fi 42 | 43 | if ! curl -vv --fail "http://localhost:$PORT/db/checks"; then 44 | # "we don't really care about the results as long as it is sucessful" 45 | echo "Call to postgrest failed" 46 | exit 1 47 | fi 48 | -------------------------------------------------------------------------------- /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/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, -1, 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 | &PubSubChecker{}, 54 | &RedisChecker{}, 55 | &ResticChecker{}, 56 | &S3Checker{}, 57 | NewNamespaceChecker(), 58 | NewPodChecker(), 59 | NewTCPChecker(), 60 | } 61 | -------------------------------------------------------------------------------- /checks/database_backup.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | 6 | "github.com/flanksource/canary-checker/api/context" 7 | "github.com/flanksource/canary-checker/api/external" 8 | v1 "github.com/flanksource/canary-checker/api/v1" 9 | "github.com/flanksource/canary-checker/pkg" 10 | ) 11 | 12 | var ( 13 | databaseScanObjectCount = prometheus.NewCounterVec( 14 | prometheus.CounterOpts{ 15 | Name: "canary_check_database_backup_scan_count", 16 | Help: "The total number of objects", 17 | }, 18 | []string{"project", "instance"}, 19 | ) 20 | databaseScanFailCount = prometheus.NewCounterVec( 21 | prometheus.CounterOpts{ 22 | Name: "canary_check_database_backup_fail_count", 23 | Help: "The number of failed backups detected", 24 | }, 25 | []string{"project", "instance"}, 26 | ) 27 | ) 28 | 29 | func init() { 30 | prometheus.MustRegister(databaseScanObjectCount, databaseScanFailCount) 31 | } 32 | 33 | type DatabaseBackupChecker struct { 34 | } 35 | 36 | func (c *DatabaseBackupChecker) Type() string { 37 | return "databasebackupcheck" 38 | } 39 | 40 | func (c *DatabaseBackupChecker) Run(ctx *context.Context) pkg.Results { 41 | var results pkg.Results 42 | for _, conf := range ctx.Canary.Spec.DatabaseBackup { 43 | results = append(results, c.Check(ctx, conf)...) 44 | } 45 | return results 46 | } 47 | 48 | func (c *DatabaseBackupChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 49 | check := extConfig.(v1.DatabaseBackupCheck) 50 | switch { 51 | case check.GCP != nil: 52 | return GCPDatabaseBackupCheck(ctx, check) 53 | default: 54 | return pkg.Invalid(check, ctx.Canary, "GCP details not provided in check") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /checks/exec.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/canary-checker/api/context" 5 | "github.com/flanksource/canary-checker/api/external" 6 | v1 "github.com/flanksource/canary-checker/api/v1" 7 | "github.com/flanksource/canary-checker/pkg" 8 | "github.com/flanksource/duty/shell" 9 | ) 10 | 11 | type ExecChecker struct { 12 | } 13 | 14 | func (c *ExecChecker) Type() string { 15 | return "exec" 16 | } 17 | 18 | func (c *ExecChecker) Run(ctx *context.Context) pkg.Results { 19 | var results pkg.Results 20 | for _, conf := range ctx.Canary.Spec.Exec { 21 | results = append(results, c.Check(ctx, conf)...) 22 | } 23 | 24 | return results 25 | } 26 | 27 | func (c *ExecChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 28 | check := extConfig.(v1.ExecCheck) 29 | result := pkg.Success(check, ctx.Canary).AddDetails(shell.ExecDetails{ExitCode: -1}) 30 | 31 | details, err := shell.Run(ctx.Context, shell.Exec{ 32 | Script: check.Script, 33 | Connections: check.Connections, 34 | Checkout: check.Checkout, 35 | EnvVars: check.EnvVars, 36 | Artifacts: check.Artifacts, 37 | }) 38 | if err != nil { 39 | return result.ErrorMessage(err).Invalidf("%v", err.Error()) 40 | } 41 | if details != nil { 42 | result.AddDetails(details) 43 | result.Artifacts = append(result.Artifacts, details.Artifacts...) 44 | 45 | if details.ExitCode != 0 { 46 | return result.Failf("%s", details.String()).ToSlice() 47 | } 48 | } 49 | 50 | return result.ToSlice() 51 | } 52 | -------------------------------------------------------------------------------- /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/folder_sftp.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/flanksource/artifacts" 5 | "github.com/flanksource/canary-checker/api/context" 6 | v1 "github.com/flanksource/canary-checker/api/v1" 7 | "github.com/flanksource/canary-checker/pkg" 8 | ) 9 | 10 | func CheckSFTP(ctx *context.Context, check v1.FolderCheck) pkg.Results { 11 | result := pkg.Success(check, ctx.Canary) 12 | var results pkg.Results 13 | results = append(results, result) 14 | 15 | if err := check.SFTPConnection.HydrateConnection(ctx); err != nil { 16 | return results.Failf("failed to populate SFTP connection: %v", err) 17 | } 18 | 19 | fs, err := artifacts.GetFSForConnection(ctx.Context, check.SFTPConnection.ToModel()) 20 | if err != nil { 21 | return results.ErrorMessage(err) 22 | } 23 | 24 | folders, err := genericFolderCheck(ctx, fs, check.Path, check.Recursive, check.Filter) 25 | if err != nil { 26 | return results.ErrorMessage(err) 27 | } 28 | result.AddDetails(folders) 29 | 30 | if test := folders.Test(check.FolderTest); test != "" { 31 | return results.Failf("%s", test) 32 | } 33 | 34 | return results 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | const mysqlCheckType = "mysql" 13 | 14 | type MysqlChecker struct{} 15 | 16 | func (c *MysqlChecker) Type() string { 17 | return mysqlCheckType 18 | } 19 | 20 | // Run: Check every entry from config according to Checker interface 21 | // Returns check result and metrics 22 | func (c *MysqlChecker) Run(ctx *context.Context) pkg.Results { 23 | var results pkg.Results 24 | for _, conf := range ctx.Canary.Spec.Mysql { 25 | results = append(results, c.Check(ctx, conf)...) 26 | } 27 | return results 28 | } 29 | 30 | func (c *MysqlChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { 31 | return CheckSQL(ctx, extConfig.(v1.MysqlCheck)) 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | var results pkg.Results 20 | return results.Failf("containerd not supported on windows") 21 | } 22 | 23 | func (c *ContainerdPullChecker) Type() string { 24 | return "containerdPull" 25 | } 26 | 27 | func (c *ContainerdPullChecker) Run(ctx *context.Context) pkg.Results { 28 | return pkg.SetupError(ctx.Canary, errors.New("containerd not supported on windows")) 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /checks/utils.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/flanksource/canary-checker/api/external" 8 | "github.com/flanksource/canary-checker/pkg" 9 | ) 10 | 11 | func Failf(check external.Check, msg string, args ...interface{}) *pkg.CheckResult { 12 | return &pkg.CheckResult{ 13 | Check: check, 14 | Pass: false, 15 | Invalid: false, 16 | Message: fmt.Sprintf(msg, args...), 17 | } 18 | } 19 | 20 | func Passf(check external.Check, msg string, args ...interface{}) *pkg.CheckResult { 21 | return &pkg.CheckResult{ 22 | Check: check, 23 | Pass: true, 24 | Invalid: false, 25 | Message: fmt.Sprintf(msg, args...), 26 | } 27 | } 28 | 29 | type NameGenerator struct { 30 | NamespacesCount int 31 | PodsCount int 32 | namespaceIndex int 33 | podIndex int 34 | mtx sync.Mutex 35 | } 36 | 37 | func (n *NameGenerator) NamespaceName(prefix string) string { 38 | n.mtx.Lock() 39 | defer n.mtx.Unlock() 40 | name := fmt.Sprintf("%s%d", prefix, n.namespaceIndex) 41 | n.namespaceIndex = (n.namespaceIndex + 1) % n.NamespacesCount 42 | return name 43 | } 44 | 45 | func (n *NameGenerator) PodName(prefix string) string { 46 | n.mtx.Lock() 47 | defer n.mtx.Unlock() 48 | name := fmt.Sprintf("%s%d", prefix, n.podIndex) 49 | n.podIndex = (n.PodsCount + 1) % n.PodsCount 50 | return name 51 | } 52 | 53 | func mb(bytes int64) string { 54 | if bytes > 1024*1024*1024 { 55 | return fmt.Sprintf("%dGB", bytes/1024/1024/1024) 56 | } else if bytes > 1024*1024 { 57 | return fmt.Sprintf("%dMB", bytes/1024/1024) 58 | } else if bytes > 1024 { 59 | return fmt.Sprintf("%dKB", bytes/1024) 60 | } 61 | return fmt.Sprintf("%dB", bytes) 62 | } 63 | -------------------------------------------------------------------------------- /checks/webhook.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | const WebhookCheckType = "webhook" 4 | -------------------------------------------------------------------------------- /cmd/offline.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/flanksource/commons/logger" 7 | "github.com/flanksource/duty" 8 | "github.com/flanksource/duty/api" 9 | "github.com/flanksource/duty/postgrest" 10 | "github.com/flanksource/duty/shutdown" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var GoOffline = &cobra.Command{ 15 | Use: "go-offline", 16 | Long: "Download all dependencies.", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | if err := postgrest.GoOffline(); err != nil { 19 | logger.Fatalf("Failed to go offline: %+v", err) 20 | } 21 | 22 | // Run in embedded mode once to download the postgres binary 23 | databaseDir := "temp-database-dir" 24 | if err := os.Mkdir(databaseDir, 0755); err != nil { 25 | logger.Fatalf("Failed to create database directory[%s]: %+v", err) 26 | } 27 | defer os.RemoveAll(databaseDir) 28 | 29 | api.DefaultConfig.ConnectionString = "embedded://" + databaseDir 30 | _, closer, err := duty.Start("embedded-temp") 31 | shutdown.AddHook(closer) 32 | if err != nil { 33 | logger.Fatalf("Failed to run in embedded mode: %+v", err) 34 | } 35 | 36 | // Intentionally exit with code 0 for Docker 37 | shutdown.ShutdownAndExit(0, "Finished downloading dependencies") 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Canary.yml 8 | - ./deploy/Topology.yml 9 | - ./deploy/Component.yml 10 | images: 11 | - name: controller 12 | newName: docker.io/flanksource/canary-checker 13 | newTag: latest 14 | -------------------------------------------------------------------------------- /config/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: canary-checker 6 | name: canary-checker 7 | -------------------------------------------------------------------------------- /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/_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/aws/aws_config_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: awsconfig-pass 5 | spec: 6 | schedule: "@every 5m" 7 | awsConfig: 8 | - name: aws config pass 9 | query: | 10 | SELECT 11 | configuration.complianceType, 12 | COUNT(*) 13 | WHERE 14 | resourceType = 'AWS::Config::ResourceCompliance' 15 | GROUP BY 16 | configuration.complianceType 17 | accessKey: 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 }}" 30 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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/cloudwatch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: cloudwatch 5 | spec: 6 | schedule: "@every 5m" 7 | cloudwatch: 8 | - name: cloudwatch test 9 | region: "eu-west-1" 10 | transform: 11 | expr: | 12 | results.MetricAlarms.filter(i, i.StateValue != 'OK' ).map(i, 13 | { 14 | 'name': i.MetricName, 15 | 'icon': 'aws-cloudwatch-alarm', 16 | 'duration': time.Since(timestamp(i.StateTransitionedTimestamp)).getMilliseconds(), 17 | 'labels': fromAWSMap(i.Dimensions). 18 | merge({'arn': i.AlarmArn}), 19 | 'message': "%s (%s) %s".format([i.AlarmDescription, i.AlarmArn, fromAWSMap(i.Dimensions)]), 20 | } 21 | ).toJSON() 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/aws/s3-protocol.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-protocol-check 5 | spec: 6 | schedule: "@every 5m" 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/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 | schedule: "@every 5m" 9 | folder: 10 | # Check for any backup not older than 7 days and min size 25 bytes 11 | - name: folder check 12 | path: s3://flanksource-public 13 | awsConnection: 14 | region: eu-central-1 15 | minSize: 50M 16 | maxAge: 10d 17 | filter: 18 | regex: .*.ova 19 | minSize: 100M 20 | # maxAge: 18760h 21 | display: 22 | template: | 23 | {{- range $f := .results.Files }} 24 | {{- if gt $f.Size 0 }} 25 | Name: {{$f.Name}} {{$f.ModTime | humanizeTime }} {{ $f.Size | humanizeBytes}} 26 | {{- end}} 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /fixtures/azure/devops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: azure-devops 5 | spec: 6 | schedule: "@every 5m" 7 | azureDevops: 8 | - name: ado test 9 | project: Demo1 10 | pipeline: ^windows- 11 | personalAccessToken: 12 | value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 13 | organization: flanksource 14 | variables: 15 | env: prod 16 | branch: 17 | - main 18 | thresholdMillis: 60000 # 60 seconds 19 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 7 | databaseBackup: 8 | - name: backup 9 | maxAge: 6h 10 | gcp: 11 | project: google-project-name 12 | instance: cloudsql-instance-name 13 | -------------------------------------------------------------------------------- /fixtures/datasources/GCP/database_backup_cred.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: database-backup-example 5 | spec: 6 | schedule: "@every 5m" 7 | databaseBackup: 8 | - name: backup 9 | maxAge: 6h 10 | gcp: 11 | project: google-project-name 12 | instance: cloudsql-instance-name 13 | gcpConnection: 14 | credentials: 15 | valueFrom: 16 | secretKeyRef: 17 | name: gcp-credentials 18 | key: AUTH_ACCESS_TOKEN 19 | -------------------------------------------------------------------------------- /fixtures/datasources/GCP/database_backup_cred_from_connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: database-backup-example 5 | spec: 6 | schedule: "@every 5m" 7 | databaseBackup: 8 | - name: backup 9 | maxAge: 6h 10 | gcp: 11 | project: google-project-name 12 | instance: cloudsql-instance-name 13 | gcpConnection: 14 | connection: connection://gcp/internal 15 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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/datasources/SFTP/sftp_fail_connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: sftp-pass 5 | spec: 6 | schedule: "@every 5m" 7 | folder: 8 | - path: /tmp/premier-league 9 | name: sample sftp check 10 | sftpConnection: 11 | connection: connection://sftp/emirates 12 | maxCount: 10 13 | -------------------------------------------------------------------------------- /fixtures/datasources/SFTP/sftp_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: sftp-pass 5 | spec: 6 | schedule: "@every 5m" 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/_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 | -------------------------------------------------------------------------------- /fixtures/datasources/_post_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | -------------------------------------------------------------------------------- /fixtures/datasources/alertmanager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: alertmanager-check 5 | spec: 6 | schedule: "@every 5m" 7 | alertmanager: 8 | - url: alertmanager.example.com 9 | name: alert-manager-transform 10 | alerts: 11 | - .* 12 | ignore: 13 | - KubeScheduler.* 14 | transform: 15 | javascript: | 16 | var out = _.map(results, function(r) { 17 | return { 18 | name: r.name, 19 | labels: r.labels, 20 | icon: 'alert', 21 | message: r.message, 22 | description: r.message, 23 | } 24 | }) 25 | JSON.stringify(out); -------------------------------------------------------------------------------- /fixtures/datasources/alertmanager_mix.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: alertmanager 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/datasources/folder_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-fail 5 | spec: 6 | schedule: "@every 5m" 7 | folder: 8 | - path: /etc/ 9 | name: min count fail 10 | minCount: 100000 11 | maxAge: 4m 12 | - path: /etc/ 13 | recursive: true 14 | name: min count recursive 15 | minCount: 100000 16 | maxAge: 4m 17 | - path: /etc/**/* 18 | name: min count glob 19 | minCount: 100000 20 | maxAge: 4m 21 | -------------------------------------------------------------------------------- /fixtures/datasources/folder_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-pass 5 | spec: 6 | schedule: "@every 5m" 7 | folder: 8 | - path: /etc/* 9 | minCount: 1 10 | name: min count glob 11 | - path: /etc/**/* 12 | minCount: 1 13 | name: min count doublestar 14 | - path: /etc/ 15 | name: Check for updated /etc files 16 | filter: 17 | # use the last known max, or 60 days ago if no last known max 18 | since: | 19 | {{- if last_result.results.max }} 20 | {{ last_result.results.max }} 21 | {{- else}} 22 | now-60d 23 | {{- end}} 24 | transform: 25 | # Save the newest modified time to the results, overriding the full file listing that would normally be saved 26 | # if no new files detected, use the last known max 27 | expr: | 28 | { 29 | "detail": { 30 | "max": string(results.?newest.modified.orValue(last_result().results.?max.orValue("now-60d"))), 31 | } 32 | }.toJSON() 33 | display: 34 | expr: results.?files.orValue([]).map(i, i.name).join(", ") 35 | test: 36 | expr: results.?files.orValue([]).size() > 0 37 | metrics: 38 | - name: new_files 39 | value: results.?files.orValue([]).size() 40 | type: counter 41 | --- 42 | apiVersion: canaries.flanksource.com/v1 43 | kind: Canary 44 | metadata: 45 | name: folder-pass-empty 46 | spec: 47 | schedule: "@every 5m" 48 | folder: 49 | - name: folder-nil-handling 50 | path: /some/folder/that/does/not/exist 51 | test: 52 | expr: results.files.size() == 0 53 | -------------------------------------------------------------------------------- /fixtures/datasources/folder_single_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-check 5 | spec: 6 | schedule: "@every 5m" 7 | folder: 8 | - path: /etc/ 9 | name: folder-check-min 10 | description: Checks if there are at least 10 files in the folder 11 | minCount: 10 -------------------------------------------------------------------------------- /fixtures/datasources/folder_with_filter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: folder-check 5 | spec: 6 | schedule: "@every 5m" 7 | folder: 8 | - name: pg-backup checks 9 | path: /data/backups 10 | filter: 11 | regex: "pg-backups-.*.zip" 12 | maxAge: 1d # require a daily backup 13 | minSize: 10mb # the backup should be at least 10mb 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/datasources/mongo_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mongo-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 5m" 9 | mongodb: 10 | - url: mongodb://mongo2.canaries.svc.cluster.local:27017/?authSource=admin 11 | name: mongo wrong password 12 | description: test mongo instance 13 | username: 14 | value: mongoadmin 15 | password: 16 | value: wronghere2 17 | -------------------------------------------------------------------------------- /fixtures/datasources/mongo_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mongo 5 | spec: 6 | schedule: "@every 5m" 7 | mongodb: 8 | - url: mongodb://$(username):$(password)@mongo.canaries.svc.cluster.local:27017/?authSource=admin 9 | name: mongo ping check 10 | username: 11 | value: mongoadmin 12 | password: 13 | value: secret 14 | -------------------------------------------------------------------------------- /fixtures/datasources/mssql_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mssql-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 5m" 9 | mssql: 10 | - url: "server=mssql.platformsystem;user id=sa;password=S0m3p@sswd;port=32010;database=master" #wrong server name for failure 11 | name: mssql servername 12 | query: "SELECT 1" 13 | results: 1 14 | -------------------------------------------------------------------------------- /fixtures/datasources/mssql_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mssql-pass 5 | spec: 6 | schedule: "@every 5m" 7 | mssql: 8 | - url: "server=mssql.canaries.svc.cluster.local;user id=$(username);password=$(password);port=1433;database=master;TrustServerCertificate=True" 9 | name: mssql pass 10 | username: 11 | value: sa 12 | password: 13 | value: S0m3p@sswd 14 | query: "SELECT 1" 15 | results: 1 16 | -------------------------------------------------------------------------------- /fixtures/datasources/mysql_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mysql-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 5m" 9 | mysql: 10 | - url: "$(username):$(password)@tcp(mysql.canaries.svc.cluster.local:3306)/mysqldb" 11 | name: mysql wrong password 12 | username: 13 | value: mysqladmin 14 | password: 15 | value: wrongpassword 16 | query: "SELECT 1" 17 | results: 1 18 | -------------------------------------------------------------------------------- /fixtures/datasources/mysql_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: mysql-pass 5 | spec: 6 | schedule: "@every 5m" 7 | mysql: 8 | - url: "mysql://$(username):$(password)@tcp(mysql.canaries.svc.cluster.local:3306)/mysqldb" 9 | name: mysql ping check 10 | username: 11 | value: mysqladmin 12 | password: 13 | value: admin123 14 | query: "SELECT 1" 15 | results: 1 16 | -------------------------------------------------------------------------------- /fixtures/datasources/posgres_stateful_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-succeed 5 | spec: 6 | schedule: "@every 5m" 7 | postgres: 8 | - name: postgres processes new 9 | url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 10 | username: 11 | value: postgresadmin 12 | password: 13 | value: admin123 14 | query: | 15 | select max(backend_start), count(*) from pg_stat_activity WHERE backend_start > 16 | {{- if last_result.results.rows }} 17 | '{{- (index last_result.results.rows 0).max }}' 18 | {{- else}} 19 | now() - interval '1 hour' 20 | {{- end}} 21 | metrics: 22 | - name: postgres_process_new 23 | type: counter 24 | value: results.rows[0].count 25 | -------------------------------------------------------------------------------- /fixtures/datasources/postgres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-check 5 | spec: 6 | schedule: '@every 30s' 7 | postgres: # or mysql, mssql 8 | - name: postgres schemas check 9 | url: "postgres://$(username):$(password)@postgres.default.svc:5432/postgres?sslmode=disable" 10 | username: 11 | valueFrom: 12 | secretKeyRef: 13 | name: postgres-credentials 14 | key: USERNAME 15 | password: 16 | valueFrom: 17 | secretKeyRef: 18 | name: postgres-credentials 19 | key: PASSWORD 20 | query: SELECT current_schemas(true) 21 | display: 22 | template: | 23 | {{- range $r := .results.rows }} 24 | {{- $r.current_schemas}} 25 | {{- end}} 26 | results: 1 27 | -------------------------------------------------------------------------------- /fixtures/datasources/postgres_empty_result_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-empty-result-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 5m" 9 | postgres: 10 | - url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 11 | name: postgres schemas check 12 | username: 13 | value: postgresadmin 14 | password: 15 | value: admin123 16 | query: SELECT 1 as name LIMIT 0 17 | markFailOnEmpty: true 18 | transform: 19 | expr: | 20 | dyn(results.rows).map(r, {'name': 'check', "pass": true}).toJSON() 21 | -------------------------------------------------------------------------------- /fixtures/datasources/postgres_empty_result_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-empty-result-pass 5 | spec: 6 | schedule: "@every 5m" 7 | postgres: 8 | - url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 9 | name: postgres schemas check 10 | username: 11 | value: postgresadmin 12 | password: 13 | value: admin123 14 | query: SELECT 1 LIMIT 0 15 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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/datasources/postgres_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: postgres-succeed 5 | spec: 6 | schedule: "@every 5m" 7 | postgres: 8 | - url: "postgres://$(username):$(password)@postgres.canaries.svc.cluster.local:5432/postgres?sslmode=disable" 9 | name: postgres schemas check 10 | username: 11 | value: postgresadmin 12 | password: 13 | value: admin123 14 | query: SELECT 1 15 | results: 1 16 | -------------------------------------------------------------------------------- /fixtures/datasources/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: prometheus 5 | spec: 6 | schedule: "@every 5m" 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/datasources/redis_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: redis-fail 5 | labels: 6 | "Expected-Fail": "true" 7 | spec: 8 | schedule: "@every 5m" 9 | redis: 10 | - addr: "redis.default--namespace:32004" #wrong host for the failure 11 | name: redis host failure 12 | db: 0 13 | description: "The redis fail test" 14 | -------------------------------------------------------------------------------- /fixtures/datasources/redis_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: redis-succeed 5 | spec: 6 | schedule: "@every 5m" 7 | redis: 8 | - addr: "redis.canaries.svc.cluster.local:6379" 9 | name: redis ping check 10 | db: 0 11 | description: "The redis pass test" 12 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /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/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 | schedule: "@every 5m" 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/elasticsearch/elasticsearch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: elasticsearch-pass 5 | spec: 6 | schedule: "@every 5m" 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/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/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 | -------------------------------------------------------------------------------- /fixtures/external/catalog.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: node-catalogs 5 | spec: 6 | schedule: '@every 30s' 7 | catalog: 8 | - name: ingress-catalog-check 9 | selector: 10 | - types: 11 | - Kubernetes::IngressClass 12 | test: 13 | expr: "size(results) > 0" 14 | 15 | -------------------------------------------------------------------------------- /fixtures/external/crossplane-kubernetes-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: crossplane-kubernetes-resource 5 | spec: 6 | schedule: "@every 10m" 7 | kubernetesResource: 8 | - name: crossplane-kubernetes-resource 9 | namespace: canaries 10 | description: "Create an S3 bucket via crossplane and run s3 check on it" 11 | waitFor: 12 | expr: 'dyn(resources).all(r, has(r.Object.status.atProvider) && has(r.Object.status.atProvider.arn))' 13 | interval: 30s 14 | timeout: 5m 15 | resources: 16 | - apiVersion: s3.aws.crossplane.io/v1beta1 17 | kind: Bucket 18 | metadata: 19 | name: check-bucket 20 | spec: 21 | forProvider: 22 | acl: private 23 | locationConstraint: us-east-1 24 | providerConfigRef: 25 | name: localstack 26 | 27 | checks: 28 | - s3: 29 | - name: s3-check 30 | bucketName: "{{ (index .resources 0).Object.metadata.name }}" 31 | objectPath: dummy 32 | region: "{{ (index .resources 0).Object.spec.forProvider.locationConstraint }}" 33 | url: http://localstack-localstack.localstack.svc.cluster.local:4566 34 | usePathStyle: true 35 | accessKey: 36 | value: test 37 | secretKey: 38 | value: test 39 | checkRetries: 40 | delay: 60s 41 | interval: 10s 42 | timeout: 5m 43 | -------------------------------------------------------------------------------- /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 | display: 18 | javascript: | 19 | var out = _.map(results, function(r) { 20 | return { 21 | name: r.title, 22 | description: r.title, 23 | labels: r.labels, 24 | severity: r.severity, 25 | } 26 | }) 27 | JSON.stringify(out); 28 | -------------------------------------------------------------------------------- /fixtures/external/pubsub-gcp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: pubsub-check 5 | spec: 6 | schedule: "@every 30s" 7 | pubsub: 8 | - name: gcp-incidents 9 | pubsub: 10 | project_id: flanksource-sandbox 11 | subscription: incident-alerts-sub 12 | transform: 13 | expr: | 14 | results.messages.map(r, gcp.incidents.toCheckResult(r)).toJSON() 15 | 16 | -------------------------------------------------------------------------------- /fixtures/git/_image: -------------------------------------------------------------------------------- 1 | flanksource/canary-checker-full 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fixtures/git/exec_checkout_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-checkout 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/git/git_check_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: github-pass 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/git/git_pull_push_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: git-pull-push 5 | spec: 6 | gitProtocol: 7 | - name: git-pull-push 8 | username: 9 | valueFrom: 10 | secretKeyRef: 11 | key: username 12 | name: gitea 13 | password: 14 | valueFrom: 15 | secretKeyRef: 16 | key: password 17 | name: gitea 18 | repository: http://gitea-http.gitea:3000/gitea_admin/test_repo.git 19 | schedule: "@every 5m" 20 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 7 | github: 8 | - query: > 9 | SELECT * FROM github_repo_checks('flanksource/artifacts') where branch='main' 10 | name: github-expresion-check 11 | test: 12 | expr: 'Age(results[0]["started_at"]) > Duration("10m")' 13 | githubToken: 14 | valueFrom: 15 | secretKeyRef: 16 | name: github-token 17 | key: GITHUB_TOKEN 18 | -------------------------------------------------------------------------------- /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 | valkey-cluster: 33 | enabled: false 34 | -------------------------------------------------------------------------------- /fixtures/git/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - git_check_pass.yaml 3 | - git_test_expression_pass.yaml 4 | - git_pull_push_pass.yaml 5 | -------------------------------------------------------------------------------- /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/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 | 7 | kubectl delete apiservice v1beta1.metrics.k8s.io --ignore-not-found=true 8 | -------------------------------------------------------------------------------- /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/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/k8s/cronjob_monitor.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: always-passing 6 | spec: 7 | schedule: "0 * * * *" 8 | concurrencyPolicy: Forbid 9 | successfulJobsHistoryLimit: 1 10 | jobTemplate: 11 | spec: 12 | backoffLimit: 1 13 | template: 14 | spec: 15 | containers: 16 | - name: fail 17 | image: busybox:1.28 18 | imagePullPolicy: IfNotPresent 19 | command: 20 | - /bin/sh 21 | - -c 22 | - exit 0 # always fail 23 | restartPolicy: OnFailure 24 | --- 25 | apiVersion: canaries.flanksource.com/v1 26 | kind: Canary 27 | metadata: 28 | name: monitor-always-passing-job 29 | spec: 30 | schedule: "@every 1m" 31 | kubernetes: 32 | - name: "Monitor always-passing job" 33 | kind: CronJob 34 | namespaceSelector: 35 | name: canaries 36 | resource: 37 | name: always-passing 38 | healthy: true 39 | -------------------------------------------------------------------------------- /fixtures/k8s/cronjob_monitor_fail.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: always-failing 6 | spec: 7 | schedule: "0 * * * *" 8 | concurrencyPolicy: Forbid 9 | failedJobsHistoryLimit: 1 10 | jobTemplate: 11 | spec: 12 | backoffLimit: 1 13 | template: 14 | spec: 15 | containers: 16 | - name: fail 17 | image: busybox:1.28 18 | imagePullPolicy: IfNotPresent 19 | command: 20 | - /bin/sh 21 | - -c 22 | - exit 1 # always fail 23 | restartPolicy: OnFailure 24 | --- 25 | apiVersion: canaries.flanksource.com/v1 26 | kind: Canary 27 | metadata: 28 | name: monitor-always-failing-job 29 | labels: 30 | "Expected-Fail": "true" 31 | spec: 32 | schedule: "@every 1m" 33 | kubernetes: 34 | - name: "Monitor always-failing job" 35 | kind: CronJob 36 | namespaceSelector: 37 | name: canaries 38 | resource: 39 | name: always-failing 40 | healthy: true 41 | -------------------------------------------------------------------------------- /fixtures/k8s/helm_check.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: helm-check 5 | spec: 6 | schedule: '@every 30s' 7 | helm: 8 | - name: helm check 9 | chartmuseum: http://chartmuseum.default:8080 10 | project: library 11 | auth: 12 | username: 13 | valueFrom: 14 | secretKeyRef: 15 | name: helm-credentials 16 | key: USERNAME 17 | password: 18 | valueFrom: 19 | secretKeyRef: 20 | name: helm-credentials 21 | key: PASSWORD 22 | -------------------------------------------------------------------------------- /fixtures/k8s/http_auth_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-configmap 5 | spec: 6 | http: 7 | - name: http basic auth check 8 | url: https://httpbin.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/http_auth_sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-token 5 | spec: 6 | http: 7 | - name: basic auth 8 | url: https://httpbin.flanksource.com/basic-auth/hello/world 9 | responseCodes: [200] 10 | headers: 11 | - name: Authorization 12 | valueFrom: 13 | serviceAccount: default 14 | -------------------------------------------------------------------------------- /fixtures/k8s/http_auth_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-secret 5 | spec: 6 | http: 7 | - name: http basic auth check 8 | url: https://httpbin.flanksource.com/basic-auth/hello/world 9 | responseCodes: [200] 10 | username: 11 | valueFrom: 12 | secretKeyRef: 13 | name: basic-auth 14 | key: user 15 | password: 16 | valueFrom: 17 | secretKeyRef: 18 | name: basic-auth 19 | key: pass 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-check-inline-kubeconfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: pod-access-check 5 | spec: 6 | schedule: "@every 5m" 7 | kubernetes: 8 | - name: pod access on aws cluster 9 | namespace: default 10 | kubeconfig: 11 | value: | 12 | apiVersion: v1 13 | clusters: 14 | - cluster: 15 | certificate-authority-data: xxxxx 16 | server: https://xxxxx.sk1.eu-west-1.eks.amazonaws.com 17 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 18 | contexts: 19 | - context: 20 | cluster: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 21 | namespace: mission-control 22 | user: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 23 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 24 | current-context: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 25 | kind: Config 26 | preferences: {} 27 | users: 28 | - name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 29 | user: 30 | exec: 31 | .... 32 | kind: Pod 33 | ready: true 34 | namespaceSelector: 35 | name: default -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-check-kubeconfig-from-file.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-access-check 6 | spec: 7 | schedule: "@every 5m" 8 | kubernetes: 9 | - name: pod access on aws cluster 10 | namespace: default 11 | kubeconfig: 12 | value: /root/.kube/aws-kubeconfig 13 | kind: Pod 14 | ready: true 15 | namespaceSelector: 16 | name: default 17 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-check-kubeconfig-from-secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-access-check 6 | spec: 7 | schedule: "@every 5m" 8 | kubernetes: 9 | - name: pod access on aws cluster 10 | namespace: default 11 | description: "deploy httpbin" 12 | kubeconfig: 13 | valueFrom: 14 | secretKeyRef: 15 | name: aws-kubeconfig 16 | key: kubeconfig 17 | kind: Pod 18 | ready: true 19 | namespaceSelector: 20 | name: default 21 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 7 | kubernetes: 8 | - name: kube-system 9 | kind: Pod 10 | healthy: true 11 | # resource: 12 | # search: labels.app=test 13 | # OR 14 | # labelSelector: k8s-app=kube-dns 15 | namespaceSelector: 16 | name: kube-*,!*lease 17 | # name: "*" 18 | display: 19 | expr: | 20 | dyn(results). 21 | map(i, i.Object). 22 | filter(i, !k8s.isHealthy(i)). 23 | map(i, "%s/%s -> %s".format([i.metadata.namespace, i.metadata.name, k8s.getHealth(i).message])).join('\n') 24 | test: 25 | expr: dyn(results).all(x, k8s.isHealthy(x)) 26 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-resource-check-inline-kubeconfig.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-creation-test 6 | spec: 7 | schedule: "@every 5m" 8 | kubernetesResource: 9 | - name: pod creation on aws cluster 10 | namespace: default 11 | description: "deploy httpbin" 12 | kubeconfig: 13 | value: | 14 | apiVersion: v1 15 | clusters: 16 | - cluster: 17 | certificate-authority-data: xxxxx 18 | server: https://xxxxx.sk1.eu-west-1.eks.amazonaws.com 19 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 20 | contexts: 21 | - context: 22 | cluster: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 23 | namespace: mission-control 24 | user: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 25 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 26 | current-context: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 27 | kind: Config 28 | preferences: {} 29 | users: 30 | - name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 31 | user: 32 | exec: 33 | .... 34 | resources: 35 | - apiVersion: v1 36 | kind: Pod 37 | metadata: 38 | name: httpbin 39 | namespace: default 40 | labels: 41 | app: httpbin 42 | spec: 43 | containers: 44 | - name: httpbin 45 | image: "kennethreitz/httpbin:latest" 46 | ports: 47 | - containerPort: 80 -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-resource-check-kubeconfig-from-file.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-creation-test 6 | spec: 7 | schedule: "@every 5m" 8 | kubernetesResource: 9 | - name: pod creation on aws cluster 10 | namespace: default 11 | description: "deploy httpbin" 12 | kubeconfig: 13 | value: /root/.kube/aws-kubeconfig 14 | resources: 15 | - apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: httpbin 19 | namespace: default 20 | labels: 21 | app: httpbin 22 | spec: 23 | containers: 24 | - name: httpbin 25 | image: "kennethreitz/httpbin:latest" 26 | ports: 27 | - containerPort: 80 28 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes-resource-check-kubeconfig-from-secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: pod-creation-test 6 | spec: 7 | schedule: "@every 5m" 8 | kubernetesResource: 9 | - name: pod creation on aws cluster 10 | namespace: default 11 | description: "deploy httpbin" 12 | kubeconfig: 13 | valueFrom: 14 | secretKeyRef: 15 | name: aws-kubeconfig 16 | key: kubeconfig 17 | resources: 18 | - apiVersion: v1 19 | kind: Pod 20 | metadata: 21 | name: httpbin 22 | namespace: default 23 | labels: 24 | app: httpbin 25 | spec: 26 | containers: 27 | - name: httpbin 28 | image: "kennethreitz/httpbin:latest" 29 | ports: 30 | - containerPort: 80 31 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: kubernetes-bundle 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: kube-pass 5 | spec: 6 | schedule: "@every 5m" 7 | kubernetes: 8 | - name: k8s-ready pods 9 | kind: Pod 10 | namespaceSelector: 11 | name: canaries 12 | resource: 13 | name: k8s-check-ready 14 | - name: k8s-ready pods 15 | 16 | kind: Pod 17 | namespaceSelector: 18 | name: canaries 19 | ready: false 20 | resource: 21 | # resource name supports wildcards 22 | name: k8s-check-not-* 23 | #labelSelector: app=k8s-not-ready 24 | -------------------------------------------------------------------------------- /fixtures/k8s/kubernetes_resource_namespace_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: namespace-creation 6 | labels: 7 | "Expected-Fail": "false" 8 | spec: 9 | schedule: "@every 5m" 10 | kubernetesResource: 11 | - name: "namespace creation" 12 | namespace: "default" 13 | description: "create a namespace and pod in it" 14 | waitFor: 15 | timeout: 3m 16 | delete: true 17 | staticResources: 18 | - apiVersion: v1 19 | kind: Namespace 20 | metadata: 21 | name: test 22 | resources: 23 | - apiVersion: v1 24 | kind: Pod 25 | metadata: 26 | name: httpbin 27 | namespace: test 28 | labels: 29 | app: httpbin 30 | spec: 31 | containers: 32 | - name: httpbin 33 | image: "kennethreitz/httpbin:latest" 34 | ports: 35 | - containerPort: 80 36 | -------------------------------------------------------------------------------- /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 | labels: 7 | "Expected-Fail": "false" 8 | spec: 9 | schedule: "@every 5m" 10 | kubernetesResource: 11 | - name: "pod exit code" 12 | description: "Create pod & check its exit code" 13 | namespace: default 14 | display: 15 | expr: | 16 | "Result of check 'exit-code-check': " + display["exit-code-check"] 17 | resources: 18 | - apiVersion: v1 19 | kind: Pod 20 | metadata: 21 | name: "hello-world-{{strings.ToLower (random.Alpha 10)}}" 22 | namespace: default 23 | spec: 24 | restartPolicy: Never 25 | containers: 26 | - name: hello-world 27 | image: hello-world 28 | waitFor: 29 | expr: "dyn(resources).all(r, k8s.isHealthy(r))" 30 | interval: "1s" 31 | timeout: "20s" 32 | checkRetries: 33 | delay: 2s 34 | timeout: 5m 35 | checks: 36 | - kubernetes: 37 | - name: exit-code-check 38 | kind: Pod 39 | namespaceSelector: 40 | name: default 41 | resource: 42 | name: "{{(index .resources 0).Object.metadata.name}}" 43 | test: 44 | expr: > 45 | size(results) == 1 && 46 | results[0].Object.status.containerStatuses[0].state.terminated.exitCode == 0 47 | -------------------------------------------------------------------------------- /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 | labels: 7 | "Expected-Fail": "true" 8 | spec: 9 | schedule: "@every 5m" 10 | kubernetesResource: 11 | - name: invalid service configuration 12 | namespace: default 13 | description: "deploy httpbin & check that it's accessible via service" 14 | waitFor: 15 | interval: 5s 16 | timeout: 30s 17 | resources: 18 | - apiVersion: v1 19 | kind: Pod 20 | metadata: 21 | name: httpbin 22 | namespace: default 23 | labels: 24 | app: httpbin-faulty 25 | spec: 26 | containers: 27 | - name: httpbin 28 | image: "kennethreitz/httpbin:latest" 29 | ports: 30 | - containerPort: 80 31 | - apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: httpbin-faulty-svc 35 | namespace: default 36 | spec: 37 | selector: 38 | app: httpbin-faulty 39 | ports: 40 | - port: 8080 41 | targetPort: 8080 42 | checks: 43 | - http: 44 | - name: Call httpbin service 45 | url: "http://httpbin-faulty-svc.default.svc" 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | - vcluster-canary-checker.yaml 14 | - kubernetes_bundle.yaml 15 | - kubernetes_resource_ingress_pass.yaml 16 | - kubernetes_resource_namespace_pass.yaml 17 | - kubernetes_resource_pod_exit_code_pass.yaml 18 | - kubernetes_resource_postgresql_helmrelease.yaml 19 | - kubernetes_resource_service_fail.yaml 20 | - kubernetes_resource_service_pass.yaml 21 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 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/k8s/pod_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: pod-pass 5 | spec: 6 | schedule: "@every 5m" 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/k8s/secret_sanitize_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: sanitize-fail 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - name: Login Check 9 | url: https://httpbin.flanksource.com/ 10 | env: 11 | - name: password 12 | value: tester123 13 | responseCodes: [200] 14 | templateBody: true 15 | body: | 16 | { 17 | "user": "admin{{test | ", 18 | "password": "{{.password}}" 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/k8s/slow/namespace_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: namespace-pass 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /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/ldap/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: canaries 4 | resources: 5 | - _setup.yaml 6 | - ldap_pass.yaml 7 | -------------------------------------------------------------------------------- /fixtures/ldap/ldap_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: ldap-pass 5 | spec: 6 | schedule: "@every 5m" 7 | ldap: 8 | - url: ldap://apacheds.canaries.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.canaries.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/minimal/cel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: cel-test 5 | spec: 6 | schedule: "@every 5m" 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/minimal/containerd-pull.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: containerd-pull-check 5 | spec: 6 | schedule: '@every 30s' 7 | containerd: # use docker if running outside kubernetes / docker 8 | - name: pull image 9 | image: docker.io/library/busybox:1.31.1 10 | expectedDigest: sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209 11 | expectedSize: 764556 12 | -------------------------------------------------------------------------------- /fixtures/minimal/containerd-push.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: containerd-push-check 5 | spec: 6 | schedule: '@every 30s' 7 | containerdPush: # use dockerPush if running outside kubernetes / docker 8 | - name: ContainerdPush Check 9 | image: docker.io/library/busybox:1.31.1 10 | username: 11 | password: -------------------------------------------------------------------------------- /fixtures/minimal/display-with-cel_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: currency-converter-display-cel 6 | spec: 7 | http: 8 | - name: USD 9 | url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS,ZAR 10 | display: 11 | expr: "'$1 = €' + string(json.rates.EUR) + ', £' + string(json.rates.GBP) + ', ₪' + string(json.rates.ILS)" 12 | -------------------------------------------------------------------------------- /fixtures/minimal/display-with-gotemplate_pass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Canary 4 | metadata: 5 | name: currency-converter-display-gotemplate 6 | spec: 7 | http: 8 | - name: USD 9 | url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS,ZAR 10 | display: 11 | template: "$1 = €{{.json.rates.EUR}}, £{{.json.rates.GBP}}, ₪{{.json.rates.ILS}}" 12 | -------------------------------------------------------------------------------- /fixtures/minimal/display-with-javascript_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: currency-converter-display-js 5 | spec: 6 | http: 7 | - name: USD 8 | url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS 9 | display: 10 | javascript: | 11 | currencyCodes = { "EUR": "€", "GBP": "£", "ILS": "₪"} 12 | 13 | display = [] 14 | for (var currency in json.rates) { 15 | display.push(currency + " = " + currencyCodes[currency] + json.rates[currency]) 16 | } 17 | 18 | // final output to display 19 | "$1 = " + display.join(", ") 20 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_artifact.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-artifact 5 | spec: 6 | schedule: "@every 5m" 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/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 | schedule: "@every 5m" 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/exec_env_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-env 5 | spec: 6 | schedule: "@every 5m" 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/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 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/minimal/exec_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: exec-pass 5 | spec: 6 | schedule: "@every 5m" 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/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 | schedule: "@every 5m" 9 | http: 10 | - url: https://httpbin.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.flanksource.com/status/202 17 | name: http-pass-multiple 18 | labels: 19 | check: http-202 20 | responseCodes: [201, 202, 301] 21 | -------------------------------------------------------------------------------- /fixtures/minimal/http-crawl_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-crawler 5 | spec: 6 | schedule: "@daily" 7 | http: 8 | - name: docs 9 | url: http://httpbin.org/ 10 | display: 11 | expr: missing.join("\n") 12 | crawl: 13 | parallelism: 10 14 | delay: 1ms 15 | randomDelay: 1ms 16 | depth: 10 17 | -------------------------------------------------------------------------------- /fixtures/minimal/http-templateBody.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-check-template 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - name: template body 9 | url: https://httpbin.flanksource.com/post 10 | method: POST 11 | templateBody: true 12 | body: | 13 | { 14 | "name": "{{.metadata.name}}" 15 | } 16 | display: 17 | expr: json['json'] 18 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_from_config_map.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth 5 | spec: 6 | schedule: "@every 1m" 7 | http: 8 | - name: "basic auth fail" 9 | url: https://httpbin.flanksource.com/basic-auth/hello/world 10 | responseCodes: [401] 11 | - name: "basic auth pass" 12 | url: https://httpbin.flanksource.com/basic-auth/hello/world 13 | responseCodes: [200] 14 | username: 15 | valueFrom: 16 | configMapKeyRef: 17 | name: basic-auth 18 | key: username 19 | password: 20 | valueFrom: 21 | configMapKeyRef: 22 | name: basic-auth 23 | key: password 24 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_from_helm_ref.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-from-helm 5 | spec: 6 | schedule: "@every 1m" 7 | http: 8 | - name: HTTP check 9 | url: $(url) 10 | env: 11 | - name: url 12 | valueFrom: 13 | helmRef: 14 | name: podinfo 15 | key: .ingress.hosts[0].host 16 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_from_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth 5 | spec: 6 | schedule: "@every 1m" 7 | http: 8 | - name: "basic auth fail" 9 | url: https://httpbin.flanksource.com/basic-auth/hello/world 10 | responseCodes: [401] 11 | - name: "basic auth pass" 12 | url: https://httpbin.flanksource.com/basic-auth/hello/world 13 | responseCodes: [200] 14 | username: 15 | valueFrom: 16 | secretKeyRef: 17 | name: httpbin-secret 18 | key: username 19 | password: 20 | valueFrom: 21 | secretKeyRef: 22 | name: httpbin-secret 23 | key: password 24 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_from_service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-service-account 5 | spec: 6 | schedule: "@every 1m" 7 | http: 8 | - name: vault-example-sre 9 | description: "HashiCorp Vault functionality check." 10 | url: https://vault.example/v1/auth/kubernetes/login 11 | env: 12 | - name: TOKEN 13 | valueFrom: 14 | serviceAccount: default-account 15 | templateBody: true 16 | body: | 17 | { 18 | "jwt": "$(TOKEN)", 19 | "role": "example-role" 20 | } 21 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_static_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-static 5 | spec: 6 | http: 7 | - name: "basic auth fail" 8 | url: https://httpbin.flanksource.com/basic-auth/hello/world 9 | responseCodes: [401] 10 | - name: "basic auth pass" 11 | url: https://httpbin.flanksource.com/basic-auth/hello/world 12 | responseCodes: [200] 13 | username: 14 | value: hello 15 | password: 16 | value: world 17 | -------------------------------------------------------------------------------- /fixtures/minimal/http_auth_url_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-basic-auth-url 5 | spec: 6 | http: 7 | - name: test-url-via-env 8 | # The URL can be templated from arbritrary values using the env field and $(.) syntax 9 | url: $(.url) 10 | env: 11 | - name: url 12 | value: https://hello:world2@httpbin.org/basic-auth/hello/world2 13 | - name: test-basic-via-env 14 | # the url can be constructed from multiple variables 15 | url: https://$(.user):$(.pass)@httpbin.org/basic-auth/hello/world 16 | templateBody: true 17 | body: | 18 | {{. | toJSONPretty " " }} 19 | responseCodes: [200] 20 | env: 21 | - name: user 22 | value: hello 23 | - name: pass 24 | value: world 25 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 9 | http: 10 | - url: https://httpbin.flanksource.com/status/500 11 | name: http fail response code check 12 | responseCodes: [200] 13 | - url: https://httpbin.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/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 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/minimal/http_no_auth_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-no-auth 5 | spec: 6 | http: 7 | - name: http-no-auth 8 | url: https://httpbin.flanksource.com/headers 9 | test: 10 | expr: "! ('Authorization' in json.headers.keys())" 11 | -------------------------------------------------------------------------------- /fixtures/minimal/http_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-pass 5 | labels: 6 | canary: http 7 | spec: 8 | schedule: "@every 5m" 9 | http: 10 | - url: https://httpbin.flanksource.com/status/200 11 | name: http-deprecated-endpoint 12 | - name: http-minimal-check 13 | url: https://httpbin.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.flanksource.com/status/200 27 | responseCodes: [201, 200, 301] 28 | responseContent: "" 29 | maxSSLExpiry: 7 30 | - name: http-expr-tests 31 | url: https://httpbin.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 | - name: http-headers 37 | url: https://httpbin.flanksource.com/headers 38 | test: 39 | expr: json.headers["User-Agent"].startsWith("canary-checker/") 40 | - name: http-body 41 | url: https://httpbin.flanksource.com/html 42 | responseContent: Herman Melville 43 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 8 | http: 9 | - url: https://httpbin.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.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 | -------------------------------------------------------------------------------- /fixtures/minimal/http_simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-check 5 | spec: 6 | schedule: "@every 30s" 7 | http: 8 | - name: basic-check 9 | url: https://httpbin.flanksource.com/status/200 10 | - name: failing-check 11 | url: https://httpbin.flanksource.com/status/500 12 | -------------------------------------------------------------------------------- /fixtures/minimal/http_single_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-single 5 | labels: 6 | canary: http 7 | spec: 8 | schedule: "@every 5m" 9 | http: 10 | - name: http pass response 200 status code 11 | url: https://httpbin.flanksource.com/status/200 12 | thresholdMillis: 3000 13 | responseCodes: [200] 14 | maxSSLExpiry: 7 15 | -------------------------------------------------------------------------------- /fixtures/minimal/http_template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: templated-http 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - name: templated-http 9 | url: https://webhook.site/#!/9f1392a6-718a-4ef5-a8e2-bfb55b08afca/f93d307b-0aaf-4a38-b9b3-db5daaae5657/1 10 | responseCodes: [200] 11 | templateBody: true 12 | 13 | env: 14 | - name: db 15 | valueFrom: 16 | secretKeyRef: 17 | name: db-user-pass 18 | key: username 19 | body: | 20 | { 21 | "canary": "{{.canary.name}}", 22 | "secret": "{{.db}}" 23 | } 24 | -------------------------------------------------------------------------------- /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 | schedule: "@every 5m" 9 | http: 10 | - url: https://httpbin.flanksource.com/delay/2 11 | name: http fail timeout 12 | thresholdMillis: 100 13 | responseCodes: [200] 14 | -------------------------------------------------------------------------------- /fixtures/minimal/http_tls_check_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-tls-duration 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - name: http pass response 200 status code 9 | endpoint: https://httpbin.flanksource.com/status/200 10 | test: 11 | expr: "code in [200,201,301] && sslAge > Duration('7d')" 12 | -------------------------------------------------------------------------------- /fixtures/minimal/http_tls_config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-check 5 | spec: 6 | schedule: "@every 30s" 7 | http: 8 | - name: http pass response 200 status code 9 | url: https://httpbin.flanksource.com/status/200 10 | tlsConfig: 11 | ca: 12 | valueFrom: 13 | secretKeyRef: 14 | name: ca-cert 15 | key: ca.pem 16 | -------------------------------------------------------------------------------- /fixtures/minimal/http_trace_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-check 5 | annotations: 6 | trace: "true" 7 | spec: 8 | schedule: "@every 30s" 9 | http: 10 | - name: headers check 11 | url: https://httpbin.flanksource.com/headers 12 | -------------------------------------------------------------------------------- /fixtures/minimal/icmp_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: icmp-fail 5 | spec: 6 | schedule: "@every 5m" 7 | icmp: 8 | - name: github 9 | endpoint: https://github.com 10 | thresholdMillis: 1 11 | packetLossThreshold: 5 12 | packetCount: 2 13 | -------------------------------------------------------------------------------- /fixtures/minimal/jmeter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: jmeter-check 5 | spec: 6 | schedule: "@every 30s" 7 | jmeter: 8 | - name: jmeter check 9 | host: 192.168.1.5 10 | port: 1099 11 | jmx: 12 | valueFrom: 13 | configMapKeyRef: 14 | name: jmeter-config 15 | key: sample 16 | -------------------------------------------------------------------------------- /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_single_pass.yaml 8 | - http_timeout_fail.yaml 9 | - http_auth_static_pass.yaml 10 | - display-with-cel_pass.yaml 11 | - display-with-gotemplate_pass.yaml 12 | - display-with-javascript_pass.yaml -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fixtures/minimal/metrics-transformed.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 | transform: 11 | expr: | 12 | { 13 | 'metrics': json.rates.keys().map(k, { 14 | 'name': "exchange_rate", 15 | 'type': "gauge", 16 | 'value': json.rates[k], 17 | 'labels': { 18 | "from": json.base, 19 | "to": k 20 | } 21 | }) 22 | }.toJSON() 23 | metrics: 24 | - name: exchange_rate_api 25 | type: histogram 26 | value: elapsed.getMilliseconds() 27 | -------------------------------------------------------------------------------- /fixtures/minimal/metrics.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: http-pass-single 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - name: http-minimal-check 9 | url: https://httpbin.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/minimal/namespaced_check_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: namespaced-http-check 5 | spec: 6 | schedule: "@every 5m" 7 | http: 8 | - url: https://example.com 9 | name: first-namespaced-check 10 | description: "demonstrate that you can set the namespace directly on the check" 11 | namespace: dev 12 | responseCodes: [200] 13 | - url: https://example.com 14 | name: second-check 15 | description: "demonstrate that you can override the check's namespace after transformation" 16 | responseCodes: [200] 17 | transform: 18 | expr: | 19 | { 20 | 'name': 'second-after-transformation-check', 21 | 'namespace': 'prod', 22 | 'message': 'static message', 23 | 'description': 'static description', 24 | 'pass': true, 25 | }.toJSON() 26 | -------------------------------------------------------------------------------- /fixtures/minimal/schedule_invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: schedule-invalid 5 | spec: 6 | schedule: "8 46 *" 7 | tcp: 8 | - name: "flanksource website" 9 | endpoint: www.flanksource.com:80 10 | thresholdMillis: 1200 11 | -------------------------------------------------------------------------------- /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:443 10 | icon: https://flanksource.com/docs/img/flanksource-icon.png 11 | thresholdMillis: 1200 12 | display: 13 | template: Success 14 | -------------------------------------------------------------------------------- /fixtures/minimal/tcp_invalid.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: tcp-invalid 5 | spec: 6 | schedule: "*/1 * * * *" 7 | tcp: 8 | - name: "flanksource website" 9 | endpoint: https://www.flanksource.com:80 10 | thresholdMillis: 1200 11 | -------------------------------------------------------------------------------- /fixtures/namespace.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: canaries 5 | -------------------------------------------------------------------------------- /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/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/opensearch/opensearch_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: opensearch-fail 5 | labels: 6 | canary: opensearch 7 | "Expected-Fail": "true" 8 | spec: 9 | schedule: "@every 30s" 10 | opensearch: 11 | - name: opensearch_fail 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.role": "api" 20 | } 21 | } 22 | } 23 | results: 100 24 | -------------------------------------------------------------------------------- /fixtures/opensearch/opensearch_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: opensearch-pass 5 | labels: 6 | canary: opensearch 7 | spec: 8 | schedule: "@every 30s" 9 | opensearch: 10 | - name: opensearch_pass 11 | description: OpenSearch checker 12 | url: http://opensearch.canaries.svc.cluster.local:9200 13 | index: index 14 | query: | 15 | { 16 | "query": { 17 | "term": { 18 | "system.version": "v1.0" 19 | } 20 | } 21 | } 22 | results: 1 23 | -------------------------------------------------------------------------------- /fixtures/prometheus/jobs-fail-only.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: prometheus-failing-jobs 5 | spec: 6 | schedule: "@every 5m" 7 | prometheus: 8 | - name: Jobs 9 | query: up{namespace!~"kube-system|monitoring"} == 0 10 | url: http://prometheus.monitoring.svc:9090 11 | transform: 12 | expr: | 13 | dyn(results).map(r, { 14 | 'name': r.job, 15 | 'namespace': 'namespace' in r ? r.namespace : '', 16 | 'labels': r.omit(["value", "__name__"]), 17 | 'pass': false 18 | }).toJSON() 19 | -------------------------------------------------------------------------------- /fixtures/prometheus/jobs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: prometheus-jobs 5 | spec: 6 | schedule: "@every 5m" 7 | prometheus: 8 | - name: Jobs 9 | query: up{namespace!~"kube-system|monitoring"} 10 | url: http://prometheus.monitoring.svc:9090 11 | transform: 12 | expr: | 13 | dyn(results).map(r, { 14 | 'name': r.job, 15 | 'namespace': 'namespace' in r ? r.namespace : '', 16 | 'labels': r.omit(["value", "__name__"]), 17 | 'pass': r["value"] > 0 18 | }).toJSON() 19 | -------------------------------------------------------------------------------- /fixtures/quarantine/icmp_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: icmp 5 | spec: 6 | schedule: "@every 5m" 7 | icmp: 8 | - name: ICMP test 9 | endpoint: api.github.com 10 | thresholdMillis: 600 11 | packetLossThreshold: 10 12 | packetCount: 2 13 | -------------------------------------------------------------------------------- /fixtures/quarantine/s3_fail.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: s3-fail 5 | spec: 6 | schedule: "@every 5m" 7 | s3: 8 | - name: s3 check 9 | bucket: "test-bucket" 10 | region: "us-east-1" 11 | endpoint: "https://test-bucket.s3.us-east-1.amazonaws.com" 12 | secretKey: 13 | value: "****************" 14 | accessKey: 15 | value: "~~~~~~~~~~~~~~~~" 16 | objectPath: "path/to/object" 17 | skipTLSVerify: true 18 | -------------------------------------------------------------------------------- /fixtures/quarantine/smb_pass.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Canary 3 | metadata: 4 | name: smb-pass 5 | spec: 6 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /fixtures/restic/_image: -------------------------------------------------------------------------------- 1 | flanksource/canary-checker-full 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | schedule: "@every 5m" 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/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 | schedule: "@every 5m" 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 | -------------------------------------------------------------------------------- /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.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.flanksource.com/status/202 30 | responseCodes: 31 | - 202 32 | name: second-inline-canary 33 | -------------------------------------------------------------------------------- /fixtures/topology/component-status-expr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Topology 4 | metadata: 5 | name: status-expr 6 | spec: 7 | icon: flux 8 | type: Topology 9 | schedule: "@every 5m" 10 | statusExpr: | 11 | summary.healthy > 0 && summary.unhealthy == 0 && summary.warning == 0 ? "healthy" : 12 | summary.healthy == 0 && summary.unhealthy == 0 && summary.warning == 0 ? "unknown" : 13 | summary.healthy > 0 && summary.unhealthy > 0 || summary.warning > 0 ? "warning" : 14 | "unhealthy" 15 | components: 16 | - icon: nodes 17 | name: Nodes 18 | components: 19 | - name: Nodes Component 20 | type: lookup 21 | lookup: 22 | catalog: 23 | - name: "" 24 | test: {} 25 | display: 26 | expr: > 27 | dyn(results).map(r, { 28 | 'name': r.name, 29 | 'icon': 'node', 30 | 'status': r.status, 31 | 'status_reason': r.description, 32 | 'selectors': [{'labelSelector': 'app.kubernetes.io/instance='+r.name}], 33 | 'statusExpr': 'summary.healthy > 0 && summary.unhealthy == 0 ? "good" : "bad"', 34 | }).toJSON() 35 | selector: 36 | - types: 37 | - Kubernetes::Node 38 | -------------------------------------------------------------------------------- /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/topology/component-with-property-list.yml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: test-topology-property-list 5 | spec: 6 | schedule: "@every 10m" 7 | components: 8 | - name: RootComponent 9 | properties: 10 | - name: error_percentage 11 | min: 0 12 | 13 | # Test property lookup merge as components 14 | - name: error_percentage_lookup 15 | lookup: 16 | http: 17 | - url: https://httpbin.flanksource.com/status/200 18 | name: error_percentage_lookup_max 19 | display: 20 | expr: | 21 | [ 22 | {'name': 'error_percentage', 'max': 100} 23 | ].toJSON() 24 | - url: https://httpbin.flanksource.com/status/200 25 | name: error_percentage_lookup_value 26 | display: 27 | expr: | 28 | {'name': 'error_percentage', 'value': 10}.toJSON() 29 | -------------------------------------------------------------------------------- /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.flanksource.com/status/202 15 | responseCodes: 16 | - 202 17 | name: inline-canary 18 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /fixtures/topology/kubernetes-lookup-inline-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: kubernetes-configs 5 | spec: 6 | type: Config 7 | icon: kubernetes 8 | schedule: "@every 5m" 9 | components: 10 | - name: configs 11 | icon: server 12 | type: ConfigMap 13 | lookup: 14 | kubernetes: 15 | - kind: ConfigMap 16 | display: 17 | expr: | 18 | dyn(results).map(c, { 19 | 'name': c.Object.metadata.name, 20 | 'type': 'ConfigMap', 21 | }).toJSON() 22 | kubeconfig: 23 | value: | 24 | apiVersion: v1 25 | clusters: 26 | - cluster: 27 | certificate-authority-data: xxxxx 28 | server: https://xxxxx.sk1.eu-west-1.eks.amazonaws.com 29 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 30 | contexts: 31 | - context: 32 | cluster: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 33 | namespace: mission-control 34 | user: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 35 | name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 36 | current-context: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 37 | kind: Config 38 | preferences: {} 39 | users: 40 | - name: arn:aws:eks:eu-west-1:765618022540:cluster/aws-cluster 41 | user: 42 | exec: 43 | .... 44 | -------------------------------------------------------------------------------- /fixtures/topology/kubernetes-lookup-kubeconfig-from-file.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: canaries.flanksource.com/v1 2 | kind: Topology 3 | metadata: 4 | name: kubernetes-configs 5 | spec: 6 | type: Config 7 | icon: kubernetes 8 | schedule: "@every 5m" 9 | components: 10 | - name: configs 11 | icon: server 12 | type: ConfigMap 13 | lookup: 14 | kubernetes: 15 | - kind: ConfigMap 16 | display: 17 | expr: | 18 | dyn(results).map(c, { 19 | 'name': c.name, 20 | 'type': 'ConfigMap', 21 | }).toJSON() 22 | kubeconfig: 23 | value: /root/.kube/aws-kubeconfig 24 | -------------------------------------------------------------------------------- /fixtures/topology/kubernetes-lookup-kubeconfig-from-secrets.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Topology 4 | metadata: 5 | name: kubernetes-configs 6 | spec: 7 | type: Config 8 | icon: kubernetes 9 | schedule: "@every 5m" 10 | components: 11 | - name: configs 12 | icon: server 13 | type: ConfigMap 14 | lookup: 15 | kubernetes: 16 | - kind: ConfigMap 17 | display: 18 | expr: | 19 | dyn(results).map(c, { 20 | 'name': c.Object.metadata.name, 21 | 'type': 'ConfigMap', 22 | }).toJSON() 23 | kubeconfig: 24 | valueFrom: 25 | secretKeyRef: 26 | name: aws-kubeconfig 27 | key: kubeconfig 28 | -------------------------------------------------------------------------------- /fixtures/topology/kubernetes-lookup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: canaries.flanksource.com/v1 3 | kind: Topology 4 | metadata: 5 | name: kubernetes-configs 6 | spec: 7 | type: Config 8 | icon: kubernetes 9 | schedule: "@every 5m" 10 | components: 11 | - name: configs 12 | icon: server 13 | type: ConfigMap 14 | lookup: 15 | kubernetes: 16 | - kind: ConfigMap 17 | display: 18 | expr: | 19 | dyn(results).map(c, { 20 | 'name': c.Object.metadata.name, 21 | 'type': 'ConfigMap', 22 | }).toJSON() 23 | -------------------------------------------------------------------------------- /fixtures/topology/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - k8s-system.yaml -------------------------------------------------------------------------------- /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.flanksource.com/status/200 21 | test: 22 | expr: "code == 200" 23 | name: selector 24 | type: aggregator 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | */ -------------------------------------------------------------------------------- /hack/generate-schemas/.gitignore: -------------------------------------------------------------------------------- 1 | go.mod 2 | go.sum 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/runner" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | version = "dev" 14 | commit = "none" 15 | date = "unknown" 16 | ) 17 | 18 | func main() { 19 | if len(commit) > 8 { 20 | version = fmt.Sprintf("%v, commit %v, built at %v", version, commit[0:8], date) 21 | runner.Version = fmt.Sprintf("%v (%v)", version, commit[0:8]) 22 | } else { 23 | runner.Version = version 24 | } 25 | 26 | cmd.Root.AddCommand(&cobra.Command{ 27 | Use: "version", 28 | Short: "Print the version of canary-checker", 29 | Args: cobra.MinimumNArgs(0), 30 | Run: func(cmd *cobra.Command, args []string) { 31 | fmt.Println(version) 32 | }, 33 | }) 34 | 35 | cmd.Root.SetUsageTemplate(cmd.Root.UsageTemplate() + fmt.Sprintf("\nversion: %s\n ", version)) 36 | 37 | if err := cmd.Root.Execute(); err != nil { 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /osv-scanner.toml: -------------------------------------------------------------------------------- 1 | # OSV-Scanner Configuration 2 | # Used by OpenSSF Scorecard for vulnerability scanning 3 | # Documentation: https://google.github.io/osv-scanner/configuration/ 4 | 5 | # Ignore specific vulnerabilities that are accepted risks 6 | [[IgnoredVulns]] 7 | id = "GO-2022-0635" 8 | # In-band key negotiation issue in AWS S3 Crypto SDK 9 | # Reason: Not called by our codebase (verified with govulncheck) 10 | # Module: github.com/aws/aws-sdk-go (indirect dependency only) 11 | # Review Date: 2025-10-29 12 | reason = "Not exploitable - S3 crypto functions not used by our code. See SECURITY.md for details." 13 | 14 | [[IgnoredVulns]] 15 | id = "GO-2022-0646" 16 | # CBC padding oracle issue in AWS S3 Crypto SDK 17 | # Reason: Not called by our codebase (verified with govulncheck) 18 | # Module: github.com/aws/aws-sdk-go (indirect dependency only) 19 | # Review Date: 2025-10-29 20 | reason = "Not exploitable - S3 crypto functions not used by our code. See SECURITY.md for details." 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | v1 "github.com/flanksource/canary-checker/api/v1" 5 | "github.com/flanksource/canary-checker/pkg/prometheus" 6 | "github.com/flanksource/commons/collections" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ) 9 | 10 | // OperatorExecutor when true means the application is serving as the k8s operator 11 | var OperatorExecutor bool 12 | 13 | var RunnerName string 14 | 15 | var Version string 16 | 17 | var RunnerLabels map[string]string = make(map[string]string) 18 | 19 | var Prometheus *prometheus.PrometheusClient 20 | 21 | // WatchNamespace is the kubernetes operator namespace. 22 | var WatchNamespace string 23 | 24 | var ( 25 | // Only canaries matching these namespace will be allowed run 26 | IncludeNamespaces []string 27 | 28 | // Only canaries matching these labels will be allowed run 29 | IncludeLabels []string 30 | 31 | // Only canaries with these names will be allowed run 32 | IncludeCanaries []string 33 | ) 34 | 35 | func IsCanaryIgnored(canary *metav1.ObjectMeta) bool { 36 | if !collections.MatchItems(canary.Namespace, IncludeNamespaces...) { 37 | return true 38 | } 39 | 40 | if !collections.MatchItems(canary.Name, IncludeCanaries...) { 41 | return true 42 | } 43 | 44 | labelSelector := collections.KeyValueSliceToMap(IncludeLabels) 45 | for k, v := range labelSelector { 46 | if lVal, ok := canary.Labels[k]; !ok { 47 | return true 48 | } else if !collections.MatchItems(lVal, v) { 49 | return true 50 | } 51 | } 52 | 53 | return false 54 | } 55 | 56 | func IsCanarySuspended(c v1.Canary) bool { 57 | return (c.Spec.Replicas != nil && *c.Spec.Replicas == 0) || 58 | (c.ObjectMeta.Annotations != nil && c.ObjectMeta.Annotations["suspend"] == "true") 59 | } 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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)) 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 | -------------------------------------------------------------------------------- /pkg/topology/utils.go: -------------------------------------------------------------------------------- 1 | package topology 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func isComponent(s map[string]interface{}) bool { 8 | _, name := s["name"] 9 | _, properties := s["properties"] 10 | return name && properties 11 | } 12 | 13 | func isProperty(s map[string]interface{}) bool { 14 | _, name := s["name"] 15 | _, properties := s["properties"] 16 | return name && !properties 17 | } 18 | 19 | func isPropertyList(data []byte) bool { 20 | var s = []map[string]interface{}{} 21 | if err := json.Unmarshal(data, &s); err != nil { 22 | return false 23 | } 24 | if len(s) == 0 { 25 | return false 26 | } 27 | return isProperty(s[0]) 28 | } 29 | 30 | func isComponentList(data []byte) bool { 31 | var s = []map[string]interface{}{} 32 | if err := json.Unmarshal(data, &s); err != nil { 33 | return false 34 | } 35 | if len(s) == 0 { 36 | return false 37 | } 38 | return isComponent(s[0]) 39 | } 40 | 41 | func genParentKey(name, _type, namespace string) string { 42 | return strings.Join([]string{"parent.key", name, _type, namespace}, "/") 43 | } 44 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/samber/lo" 8 | ) 9 | 10 | func TestParseTime(t *testing.T) { 11 | testCases := []struct { 12 | name string 13 | input string 14 | expected *time.Time 15 | }{ 16 | { 17 | name: "RFC3339", 18 | input: "2023-04-05T15:04:05Z", 19 | expected: lo.ToPtr(time.Date(2023, 4, 5, 15, 4, 5, 0, time.UTC)), 20 | }, 21 | { 22 | name: "RFC3339Nano", 23 | input: "2023-04-05T15:04:05.999999999Z", 24 | expected: lo.ToPtr(time.Date(2023, 4, 5, 15, 4, 5, 999999999, time.UTC)), 25 | }, 26 | { 27 | name: "ISO8601 with timezone", 28 | input: "2023-04-05T15:04:05+02:00", 29 | expected: lo.ToPtr(time.Date(2023, 4, 5, 15, 4, 5, 0, time.FixedZone("", 2*60*60))), 30 | }, 31 | { 32 | name: "ISO8601 without timezone", 33 | input: "2023-04-05T15:04:05", 34 | expected: lo.ToPtr(time.Date(2023, 4, 5, 15, 4, 5, 0, time.UTC)), 35 | }, 36 | { 37 | name: "MySQL datetime format", 38 | input: "2023-04-05 15:04:05", 39 | expected: lo.ToPtr(time.Date(2023, 4, 5, 15, 4, 5, 0, time.UTC)), 40 | }, 41 | { 42 | name: "Invalid format", 43 | input: "not a valid time", 44 | expected: nil, 45 | }, 46 | } 47 | 48 | for _, tc := range testCases { 49 | t.Run(tc.name, func(t *testing.T) { 50 | result := ParseTime(tc.input) 51 | if tc.expected == nil { 52 | if result != nil { 53 | t.Errorf("Expected nil, but got %v", result) 54 | } 55 | } else { 56 | if result == nil { 57 | t.Errorf("Expected %v, but got nil", tc.expected) 58 | } else if !result.Equal(*tc.expected) { 59 | t.Errorf("Expected %v, but got %v", tc.expected, result) 60 | } 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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 $(LD_FLAGS) ./ 13 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------