diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..62d232b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: Contiuous Integration + +on: + push: + pull_request: + +permissions: + contents: write + packages: write + security-events: write + +jobs: + rel: + name: Build, scan & push Snapshot + runs-on: ubuntu-latest + + permissions: + contents: write + packages: write + security-events: write + + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - name: Build snapshot artifacts + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --snapshot --clean --config .goreleaser-ci.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get Version + id: version + run: echo "value=commit-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: "ghcr.io/caas-team/sparrow:${{ steps.version.outputs.value }}" + format: "sarif" + output: "trivy-results.sarif" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: "trivy-results.sarif" + + - name: Registry login + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push snapshot container image + run: docker push ghcr.io/caas-team/sparrow:${{ steps.version.outputs.value }} \ No newline at end of file diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml new file mode 100644 index 00000000..e8a565e9 --- /dev/null +++ b/.github/workflows/end2end.yml @@ -0,0 +1,84 @@ +# This workflow installs 1 instance of sparrow and +# verify the API output + +name: End2End Testing +on: + push: + paths: + - 'chart/**' + +jobs: + end2end: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Set up K3S + uses: debianmaster/actions-k3s@master + id: k3s + with: + version: 'v1.26.9-k3s1' + - name: Check Cluster + run: | + kubectl get nodes + - name: Check Coredns Deployment + run: | + kubectl -n kube-system rollout status deployment/coredns --timeout=60s + STATUS=$(kubectl -n kube-system get deployment coredns -o jsonpath={.status.readyReplicas}) + if [[ $STATUS -ne 1 ]] + then + echo "Deployment coredns not ready" + kubectl -n kube-system get events + exit 1 + else + echo "Deployment coredns OK" + fi + - name: Check Metricsserver Deployment + run: | + kubectl -n kube-system rollout status deployment/metrics-server --timeout=60s + STATUS=$(kubectl -n kube-system get deployment metrics-server -o jsonpath={.status.readyReplicas}) + if [[ $STATUS -ne 1 ]] + then + echo "Deployment metrics-server not ready" + kubectl -n kube-system get events + exit 1 + else + echo "Deployment metrics-server OK" + fi + - name: Setup Helm + run: | + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + helm version + - name: Get Image Tag + id: version + run: echo "value=commit-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Install Sparrow + run: | + helm upgrade -i sparrow \ + --atomic \ + --timeout 300s \ + --set extraArgs.loaderType=file \ + --set extraArgs.loaderFilePath=/runconfig/checks.yaml \ + --set image.tag=${{ steps.version.outputs.value }} \ + chart + - name: Check Pods + run: | + kubectl get pods + - name: Wait for Sparrow + run: | + sleep 60 + - name: Healthcheck + run: | + kubectl create job curl --image=quay.io/curl/curl:latest -- curl -f -v -H 'Content-Type: application/json' http://sparrow:8080/v1/metrics/health + kubectl wait --for=condition=complete job/curl + STATUS=$(kubectl get job curl -o jsonpath={.status.succeeded}) + if [[ $STATUS -ne 1 ]] + then + echo "Job failed" + kubectl logs -ljob-name=curl + kubectl delete job curl + exit 1 + else + echo "Job OK" + kubectl delete job curl + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..118ed7b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release + +on: + push: + tags: + - "v[012].[0-9]+.[0-9]+" + +permissions: + contents: write + packages: write + +jobs: + main: + name: Release Sparrow + runs-on: ubuntu-latest + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build, push & release + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + helm: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Registry login + run: helm registry login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + + - name: Helm lint + run: helm lint ./chart + + - name: Helm package + run: helm package ./chart -d ./chart + + - name: Push helm package + run: helm push $(ls ./chart/*.tgz| head -1) oci://ghcr.io/${{ github.repository_owner }}/charts \ No newline at end of file diff --git a/.github/workflows/test_sast.yml b/.github/workflows/test_sast.yml new file mode 100644 index 00000000..dacd396b --- /dev/null +++ b/.github/workflows/test_sast.yml @@ -0,0 +1,23 @@ +name: Test - SAST + +on: + pull_request: + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-latest + + env: + GO111MODULE: on + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: ./... \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test_unit.yml similarity index 57% rename from .github/workflows/test.yml rename to .github/workflows/test_unit.yml index d7966bbe..3bbdc3d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test_unit.yml @@ -1,25 +1,28 @@ -name: test +name: Test - Unit on: + push: pull_request: - branches: - - main + +permissions: + contents: read jobs: test_go: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: - go-version-file: 'go.mod' + go-version-file: go.mod - name: Test run: | go mod download go install github.com/matryer/moq@v0.3.3 go generate ./... - go test --race --coverprofile cover.out -v ./... + go test --race --coverprofile cover.out -v ./... \ No newline at end of file diff --git a/.goreleaser-ci.yaml b/.goreleaser-ci.yaml new file mode 100644 index 00000000..6eeaa854 --- /dev/null +++ b/.goreleaser-ci.yaml @@ -0,0 +1,26 @@ +project_name: sparrow +snapshot: + name_template: "commit-{{ .ShortCommit }}" +builds: + - env: [CGO_ENABLED=0] + ldflags: + - -s -w -X main.version={{ .Version }} + - -extldflags "-static" + goos: + - linux + goarch: + - amd64 + - arm64 +dockers: + - image_templates: + - "ghcr.io/caas-team/sparrow:{{ .Version }}" + dockerfile: Dockerfile + build_flag_templates: + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description="This is a pre-release version. Do not use this in production!" + - --label=org.opencontainers.image.url=https://caas.telekom.de + - --label=org.opencontainers.image.source=https://github.com/caas-team/sparrow + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ .Timestamp }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses="Apache 2.0" diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..934ae813 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,37 @@ +project_name: sparrow +builds: + - env: [CGO_ENABLED=0] + ldflags: + - -s -w -X main.version={{ .Tag }} + - -extldflags "-static" + goos: + - linux + goarch: + - amd64 + - arm64 +dockers: + - image_templates: + - "ghcr.io/caas-team/sparrow:latest" + - "ghcr.io/caas-team/sparrow:{{ .Tag }}" + - "ghcr.io/caas-team/sparrow:v{{ .Major }}.{{ .Minor }}" + - "ghcr.io/caas-team/sparrow:v{{ .Major }}" + dockerfile: Dockerfile + build_flag_templates: + - --label=org.opencontainers.image.title={{ .ProjectName }} + - --label=org.opencontainers.image.description={{ .ProjectName }} + - --label=org.opencontainers.image.url=https://caas.telekom.de + - --label=org.opencontainers.image.source=https://github.com/caas-team/sparrow + - --label=org.opencontainers.image.version={{ .Version }} + - --label=org.opencontainers.image.created={{ .Timestamp }} + - --label=org.opencontainers.image.revision={{ .FullCommit }} + - --label=org.opencontainers.image.licenses="Apache 2.0" +nfpms: + - maintainer: CaaS + description: |- + Monitoring tool to gather infrastructure network information + homepage: https://github.com/caas-team + license: Apache 2.0 + formats: + - deb + - rpm + - apk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8de2cd2..fe7a6a36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,5 +8,20 @@ repos: - id: go-fumpt-repo args: [-l, -w] - id: golangci-lint-repo-mod - # concurrency 1 is needed in case your machine can't handle the golangci args: [--config, .golangci.yaml, --, --fix] + - repo: local + hooks: + - id: go-generate-repo + name: go generate + entry: go + args: [generate, ./...] + language: system + types: [go] + pass_filenames: false + always_run: true + - repo: https://github.com/norwoodj/helm-docs + rev: "v1.11.3" + hooks: + - id: helm-docs + args: + - --chart-search-root=chart diff --git a/.vscode/launch.json b/.vscode/launch.json index 7d8cb0b1..751c7cb4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,9 @@ "args": [ "run", "--config", - ".vscode/config/local.config.yaml" + "config.yaml", + "--apiAddress", + ":9090" ] }, { diff --git a/CODEOWNERS b/CODEOWNERS index fcb6b7c5..73bf39fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @y-eight @NiklasTreml @puffitos @nico151999 @lvlcn-t \ No newline at end of file +* @y-eight @NiklasTreml @puffitos @nico151999 @lvlcn-t @eumel8 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4a590d19 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.18 as prep + +RUN apk add --no-cache ca-certificates +RUN adduser \ + --disabled-password \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid 65532 \ + sparrow + + +FROM scratch +COPY --from=prep /etc/passwd /etc/passwd +COPY --from=prep /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY sparrow ./ + +USER sparrow + +ENTRYPOINT ["/sparrow", "run"] \ No newline at end of file diff --git a/NOTICE b/NOTICE index 22703024..b2be5917 100644 --- a/NOTICE +++ b/NOTICE @@ -10,4 +10,5 @@ Maximilian Schubert [y-eight], Deutsche Telekom IT GmbH Niklas Treml [niklastreml], Deutsche Telekom IT GmbH Bruno Bressi [puffitos], Deutsche Telekom IT GmbH Nico Feulner [nico151999], Deutsche Telekom IT GmbH -Tom Vendolsky [lvlcn-t], Deutsche Telekom IT GmbH \ No newline at end of file +Tom Vendolsky [lvlcn-t], Deutsche Telekom IT GmbH +Frank Kloeker [eumel8], Deutsche Telekom IT GmbH \ No newline at end of file diff --git a/README.md b/README.md index 20e3d1c8..cbad09e4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# `sparrow` aka Check Sparrow +# Sparrow - Infrastructure Monitoring

@@ -12,11 +12,13 @@ - [Container Image](#container-image) - [Helm](#helm) - [Usage](#usage) + - [Container Image](#container-image-1) - [Configuration](#configuration) - [Startup](#startup) - [Loader](#loader) - [Runtime](#runtime) - [Check: Health](#check-health) + - [Check: Latency](#check-latency) - [API](#api) - [Code of Conduct](#code-of-conduct) - [Working Language](#working-language) @@ -33,27 +35,74 @@ The `sparrow` performs several checks to monitor the health of the infrastructur 1. Health check - `health`: The `sparrow` is able perform an http-based (HTTP/1.1) health check to provided endpoints. The `sparrow` will expose its own health check endpoint as well. -2. Latency check - `rtt`: The `sparrow` is able to communicate with other `sparrow` instances to calculate the time a request takes to the target and back. The check is http (HTTP/1.1) based as well. +2. Latency check - `latency`: The `sparrow` is able to communicate with other `sparrow` instances to calculate the time a request takes to the target and back. The check is http (HTTP/1.1) based as well. ## Installation The `sparrow` is provided as an small binary & a container image. +Please see the [release notes](https://github.com/caas-team/sparrow/releases) for to get the latest version. + ### Binary -tbd +The binary is available for several distributions. Currently the binary needs to be installed from a provided bundle or source. + +```sh +curl https://github.com/caas-team/sparrow/releases/download/v${RELEASE_VERSION}/sparrow_${RELEASE_VERSION}_linux_amd64.tar.gz -Lo sparrow.tar.gz +curl https://github.com/caas-team/sparrow/releases/download/v${RELEASE_VERSION}/sparrow_${RELEASE_VERSION}_checksums.txt -Lo checksums.txt +``` + +For example release `v0.0.1`: +```sh +curl https://github.com/caas-team/sparrow/releases/download/v0.0.1/sparrow_0.0.1_linux_amd64.tar.gz -Lo sparrow.tar.gz +curl https://github.com/caas-team/sparrow/releases/download/v0.0.1/sparrow_0.0.1_checksums.txt -Lo checksums.txt +``` + +Extract the binary: +```sh +tar -xf sparrow.tar.gz +``` ### Container Image -tbd +The [sparrow container images](https://github.com/caas-team/sparrow/pkgs/container/sparrow) for dedicated [release](https://github.com/caas-team/sparrow/releases) can be found in the GitHub registry. ### Helm -tbd +Sparrow can be install via Helm Chart. The chart is provided in the GitHub registry: + +```sh +helm -n sparrow upgrade -i sparrow oci://ghcr.io/caas-team/charts/sparrow --version 1.0.0 --create-namespace +``` + +The default settings are fine for a local running configuration. With the default Helm values the sparrow loader uses a runtime configuration that is provided in a ConfigMap. The ConfigMap can be set by defining the `runtimeConfig` section. + +To be able to load the configuration during the runtime dynamically, the sparrow loader needs to be set to type `http`. + +Use the following configuration values to use a runtime configuration by the `http` loader: + +```yaml +startupConfig: + loaderType: http + loaderHttpUrl: https://url-to-runtime-config.de/api/config%2Eyaml + +runtimeConfig: {} +``` +For all available value options see [Chart README](./chart/README.md). + +Additionally check out the sparrow [configuration](#configuration) variants. ## Usage -Use `sparrow run` to execute the instance. +Use `sparrow run` to execute the instance using the binary. + +### Container Image + +Run a `sparrow` container by using e.g. `docker run ghcr.io/caas-team/sparrow`. + +Pass the available configuration arguments to the container e.g. `docker run ghcr.io/caas-team/sparrow --help`. + +Start the instance using a mounted startup configuration file e.g. `docker run -v /config:/config ghcr.io/caas-team/sparrow --config /config/config.yaml`. ## Configuration @@ -117,10 +166,35 @@ checks: healthEndpoint: false ``` -The health check will set the target status `healthy` for status code 200, otherwise `unhealthy`. +### Check: Latency -The check is re-running after a fixed delay of one minute and will perform a health request for every target. +Available configuration options: + +- `checks` + - `latency` + - `enabled` (boolean): Currently not used. + - `interval` (integer): Interval in seconds to perform the latency check. + - `timeout` (integer): Timeout in seconds for the latency check. + - `retry` + - `count` (integer): Number of retries for the latency check. + - `delay` (integer): Delay in seconds between retries for the latency check. + - `targets` (list of strings): List of targets to send latency probe. Needs to be a valid url. Can be another `sparrow` instance. Use latency endpoint, e.g. `https://sparrow-dns.telekom.de/checks/latency`. The remote `sparrow` instance needs the `latencyEndpoint` enabled. + - `latencyEndpoint` (boolean): Needs to be activated when the `sparrow` should expose its own latency endpoint. Mandatory if another `sparrow` instance wants perform a latency check. +Example configuration: +```yaml +checks: + latency: + enabled: true + interval: 1 + timeout: 3 + retry: + count: 3 + delay: 1 + targets: + - https://example.com/ + - https://google.com/ +``` ### API @@ -159,4 +233,4 @@ Licensed under the **Apache License, Version 2.0** (the "License"); you may not You may obtain a copy of the License at . -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the [LICENSE](./LICENSE) for the specific language governing permissions and limitations under the License. \ No newline at end of file +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the [LICENSE](./LICENSE) for the specific language governing permissions and limitations under the License. diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 00000000..369c54ae --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: sparrow +description: A Helm chart to install Sparrow +type: application +keywords: + - monitoring +version: 0.0.2 +appVersion: "v0.1.0" +icon: https://github.com/caas-team/sparrow/blob/main/docs/img/sparrow.png +sources: + - https://github.com/caas-team/sparrow +maintainers: + - name: eumel8 + email: f.kloeker@telekom.de + url: https://www.telekom.com + - name: y-eight + email: maximilian.schubert@telekom.de + url: https://www.telekom.com diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 00000000..96f25450 --- /dev/null +++ b/chart/README.md @@ -0,0 +1,62 @@ +# sparrow + +![Version: 0.0.2](https://img.shields.io/badge/Version-0.0.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.1.0](https://img.shields.io/badge/AppVersion-v0.1.0-informational?style=flat-square) + +A Helm chart to install Sparrow + +## Maintainers + +| Name | Email | Url | +| ---- | ------ | --- | +| eumel8 | | | +| y-eight | | | + +## Source Code + +* + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| env | object | `{}` | | +| extraArgs | object | `{"loaderFilePath":"/runconfig/checks.yaml","loaderType":"file"}` | extra command line start parameters see: https://github.com/caas-team/sparrow/blob/main/docs/sparrow_run.md | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"ghcr.io/caas-team/sparrow"` | | +| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | +| imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"chart-example.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | +| ingress.tls | list | `[]` | | +| nameOverride | string | `""` | | +| networkPolicies | object | `{"proxy":{"enabled":false}}` | define a network policy that will open egress traffic to a proxy | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext.fsGroup | int | `1000` | | +| podSecurityContext.supplementalGroups[0] | int | `1000` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| runtimeConfig | object | `{"health":{"enabled":true,"healthEndpoint":false,"targets":["https://www.example.com/","https://www.google.com/"]},"latency":{"enabled":true,"interval":1,"retry":{"count":3,"delay":1},"targets":["https://example.com/","https://google.com/"],"timeout":3}}` | runtime configuration of the Sparrow see: https://github.com/caas-team/sparrow#runtime | +| securityContext.allowPrivilegeEscalation | bool | `false` | | +| securityContext.capabilities.drop[0] | string | `"ALL"` | | +| securityContext.privileged | bool | `false` | | +| securityContext.readOnlyRootFilesystem | bool | `true` | | +| securityContext.runAsGroup | int | `1000` | | +| securityContext.runAsUser | int | `1000` | | +| service.port | int | `8080` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.automount | bool | `true` | Automatically mount a ServiceAccount's API credentials? | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created | +| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| tolerations | list | `[]` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3) diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 00000000..7cd32abb --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,68 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "sparrow.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "sparrow.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "sparrow.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "sparrow.labels" -}} +helm.sh/chart: {{ include "sparrow.chart" . }} +{{ include "sparrow.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "sparrow.selectorLabels" -}} +app.kubernetes.io/name: {{ include "sparrow.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "sparrow.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "sparrow.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "startupConfig" -}} +{{- range $key, $value := .Values.startupConfig }} +{{ $key }}: {{ $value }} +{{- end }} +{{- end }} diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml new file mode 100644 index 00000000..ff03e768 --- /dev/null +++ b/chart/templates/configmap.yaml @@ -0,0 +1,35 @@ +{{- if .Values.runtimeConfig}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "sparrow.fullname" . }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} +data: + checks.yaml: | + apiVersion: 0.0.1 + kind: Config + checks: + {{- if .Values.runtimeConfig.health}} + health: + enabled: {{ .Values.runtimeConfig.health.enabled }} + targets: + {{- with .Values.runtimeConfig.health.targets }} + {{- toYaml . | nindent 10 }} + {{- end }} + healthEndpoint: {{ .Values.runtimeConfig.health.healthEndpoint }} + {{- end }} + {{- if .Values.runtimeConfig.latency }} + latency: + enabled: true + interval: {{ .Values.runtimeConfig.latency.interval | default 1 }} + timeout: {{ .Values.runtimeConfig.latency.timeout | default 3 }} + retry: + count: {{ .Values.runtimeConfig.latency.retry.count | default 3 }} + delay: {{ .Values.runtimeConfig.latency.retry.delay | default 1 }} + targets: + {{- with .Values.runtimeConfig.latency.targets }} + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 00000000..f5aa3b20 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,100 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "sparrow.fullname" . }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "sparrow.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "sparrow.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "sparrow.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + {{- if or .Values.extraArgs .Values.startupConfig}} + - args: + {{- end }} + {{- if .Values.startupConfig}} + - --config + - /startupconfig/.sparrow.yaml + {{- else if .Values.extraArgs }} + {{- range $key, $value := .Values.extraArgs }} + - --{{ $key }} + - {{ $value }} + {{- end }} + {{- end }} + {{- if .Values.env }} + env: + {{- range $key, $val := .Values.env }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + {{- end }} + name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port | default 8080 }} + protocol: TCP + {{- if .Values.resources }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- end }} + {{- if or .Values.runtimeConfig .Values.startupConfig}} + volumeMounts: + {{- end }} + {{- if .Values.startupConfig}} + - name: startup + mountPath: /startupconfig + {{- end }} + {{- if .Values.runtimeConfig}} + - name: runtime + mountPath: /runconfig + {{- end }} + {{- if or .Values.runtimeConfig .Values.startupConfig}} + volumes: + {{- end }} + {{- if .Values.startupConfig}} + - name: startup + secret: + secretName: {{ include "sparrow.fullname" . }} + {{- end }} + {{- if .Values.runtimeConfig}} + - name: runtime + configMap: + name: {{ include "sparrow.fullname" . }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 00000000..1303ce40 --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "sparrow.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/chart/templates/networkpolicy.yaml b/chart/templates/networkpolicy.yaml new file mode 100644 index 00000000..a587008e --- /dev/null +++ b/chart/templates/networkpolicy.yaml @@ -0,0 +1,19 @@ +{{- if .Values.networkPolicies.proxy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "sparrow.fullname" . }}-proxy-np + labels: + {{- include "sparrow.labels" . | nindent 4 }} +spec: + egress: + - ports: + - port: {{ .Values.networkPolicies.proxy.port }} + protocol: TCP + to: + - ipBlock: + cidr: {{ .Values.networkPolicies.proxy.ip }}/32 + podSelector: {} + policyTypes: + - Egress +{{- end }} \ No newline at end of file diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml new file mode 100644 index 00000000..335577af --- /dev/null +++ b/chart/templates/secret.yaml @@ -0,0 +1,11 @@ +{{- if .Values.startupConfig}} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ include "sparrow.fullname" . }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} +data: + .sparrow.yaml: {{ include "startupConfig" . | b64enc }} +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 00000000..9b87cb19 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "sparrow.fullname" . }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "sparrow.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 00000000..7ac8c31c --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "sparrow.serviceAccountName" . }} + labels: + {{- include "sparrow.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 00000000..002fb26a --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,133 @@ +# Default values for sparrow. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: ghcr.io/caas-team/sparrow + pullPolicy: IfNotPresent + # -- Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # -- Specifies whether a service account should be created + create: true + # -- Automatically mount a ServiceAccount's API credentials? + automount: true + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + fsGroup: 1000 + supplementalGroups: + - 1000 + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +env: + {} + # HTTP_PROXY: + # HTTPS_PROXY: + # NO_PROXY: + +# -- define a network policy that will +# open egress traffic to a proxy +networkPolicies: + proxy: + enabled: false + # ip: 1.2.3.4 + # port: 8080 + +resources: {} +# resources: +# limits: +# cpu: 500m +# memory: 512Mi +# requests: +# cpu: 100m +# memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# -- extra command line start parameters +# see: https://github.com/caas-team/sparrow/blob/main/docs/sparrow_run.md +extraArgs: + loaderFilePath: /runconfig/checks.yaml + loaderType: file + +# -- startup configuration of the Sparrow +# see: https://github.com/caas-team/sparrow/blob/main/docs/sparrow_run.md +# startupConfig: +# apiAddress: +# loaderFilePath: /runconfig/checks.yaml +# loaderHttpRetryCount: +# loaderHttpRetryDelay: +# loaderHttpTimeout: +# loaderHttpToken: +# loaderHttpUrl: +# loaderInterval: +# loaderType: http | file + +# -- runtime configuration of the Sparrow +# see: https://github.com/caas-team/sparrow#runtime +runtimeConfig: + health: + enabled: true + targets: + - "https://www.example.com/" + - "https://www.google.com/" + healthEndpoint: false + latency: + enabled: true + interval: 1 + timeout: 3 + retry: + count: 3 + delay: 1 + targets: + - https://example.com/ + - https://google.com/ diff --git a/cmd/run.go b/cmd/run.go index e2109c72..aa330eea 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -39,7 +39,7 @@ const ( // NewCmdRun creates a new run command func NewCmdRun() *cobra.Command { flagMapping := config.RunFlagsNameMapping{ - ApiListeningAddress: "apiListeningAddress", + ApiAddress: "apiAddress", LoaderType: "loaderType", LoaderInterval: "loaderInterval", LoaderHttpUrl: "loaderHttpUrl", @@ -57,8 +57,9 @@ func NewCmdRun() *cobra.Command { Run: run(&flagMapping), } - cmd.PersistentFlags().String(flagMapping.ApiListeningAddress, ":8080", "api: The address the server is listening on") - cmd.PersistentFlags().StringP(flagMapping.LoaderType, "l", "http", "defines the loader type that will load the checks configuration during the runtime. The fallback is the fileLoader") + cmd.PersistentFlags().String(flagMapping.ApiAddress, ":8080", "api: The address the server is listening on") + cmd.PersistentFlags().StringP(flagMapping.LoaderType, "l", "http", + "defines the loader type that will load the checks configuration during the runtime. The fallback is the fileLoader") cmd.PersistentFlags().Int(flagMapping.LoaderInterval, defaultLoaderInterval, "defines the interval the loader reloads the configuration in seconds") cmd.PersistentFlags().String(flagMapping.LoaderHttpUrl, "", "http loader: The url where to get the remote configuration") cmd.PersistentFlags().String(flagMapping.LoaderHttpToken, "", "http loader: Bearer token to authenticate the http endpoint") @@ -67,7 +68,7 @@ func NewCmdRun() *cobra.Command { cmd.PersistentFlags().Int(flagMapping.LoaderHttpRetryDelay, defaultHttpRetryDelay, "http loader: The initial delay between retries in seconds") cmd.PersistentFlags().String(flagMapping.LoaderFilePath, "config.yaml", "file loader: The path to the file to read the runtime config from") - _ = viper.BindPFlag(flagMapping.ApiListeningAddress, cmd.PersistentFlags().Lookup(flagMapping.ApiListeningAddress)) + _ = viper.BindPFlag(flagMapping.ApiAddress, cmd.PersistentFlags().Lookup(flagMapping.ApiAddress)) _ = viper.BindPFlag(flagMapping.LoaderType, cmd.PersistentFlags().Lookup(flagMapping.LoaderType)) _ = viper.BindPFlag(flagMapping.LoaderInterval, cmd.PersistentFlags().Lookup(flagMapping.LoaderInterval)) _ = viper.BindPFlag(flagMapping.LoaderHttpUrl, cmd.PersistentFlags().Lookup(flagMapping.LoaderHttpUrl)) @@ -88,7 +89,7 @@ func run(fm *config.RunFlagsNameMapping) func(cmd *cobra.Command, args []string) cfg := config.NewConfig() - cfg.SetApiListeningAddress(viper.GetString(fm.ApiListeningAddress)) + cfg.SetApiAddress(viper.GetString(fm.ApiAddress)) cfg.SetLoaderType(viper.GetString(fm.LoaderType)) cfg.SetLoaderInterval(viper.GetInt(fm.LoaderInterval)) diff --git a/docs/img/sparrow.png b/docs/img/sparrow.png new file mode 100644 index 00000000..7e720bbf Binary files /dev/null and b/docs/img/sparrow.png differ diff --git a/docs/sparrow.md b/docs/sparrow.md index c562bbcb..d410f2de 100644 --- a/docs/sparrow.md +++ b/docs/sparrow.md @@ -20,4 +20,4 @@ The check results are exposed via an API. * [sparrow gen-docs](sparrow_gen-docs.md) - Generate markdown documentation * [sparrow run](sparrow_run.md) - Run sparrow -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_completion.md b/docs/sparrow_completion.md index 861c5991..e8864bdd 100644 --- a/docs/sparrow_completion.md +++ b/docs/sparrow_completion.md @@ -28,4 +28,4 @@ See each sub-command's help for details on how to use the generated script. * [sparrow completion powershell](sparrow_completion_powershell.md) - Generate the autocompletion script for powershell * [sparrow completion zsh](sparrow_completion_zsh.md) - Generate the autocompletion script for zsh -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_completion_bash.md b/docs/sparrow_completion_bash.md index fec6a565..0fca548e 100644 --- a/docs/sparrow_completion_bash.md +++ b/docs/sparrow_completion_bash.md @@ -47,4 +47,4 @@ sparrow completion bash * [sparrow completion](sparrow_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_completion_fish.md b/docs/sparrow_completion_fish.md index 841b80c0..04963139 100644 --- a/docs/sparrow_completion_fish.md +++ b/docs/sparrow_completion_fish.md @@ -38,4 +38,4 @@ sparrow completion fish [flags] * [sparrow completion](sparrow_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_completion_powershell.md b/docs/sparrow_completion_powershell.md index 47bbfeb6..20cfbe44 100644 --- a/docs/sparrow_completion_powershell.md +++ b/docs/sparrow_completion_powershell.md @@ -35,4 +35,4 @@ sparrow completion powershell [flags] * [sparrow completion](sparrow_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_completion_zsh.md b/docs/sparrow_completion_zsh.md index d756746e..d109b180 100644 --- a/docs/sparrow_completion_zsh.md +++ b/docs/sparrow_completion_zsh.md @@ -49,4 +49,4 @@ sparrow completion zsh [flags] * [sparrow completion](sparrow_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_gen-docs.md b/docs/sparrow_gen-docs.md index 1c512efe..1c456179 100644 --- a/docs/sparrow_gen-docs.md +++ b/docs/sparrow_gen-docs.md @@ -27,4 +27,4 @@ sparrow gen-docs [flags] * [sparrow](sparrow.md) - Sparrow, the infrastructure monitoring agent -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/docs/sparrow_run.md b/docs/sparrow_run.md index 5b94ad00..df6233c5 100644 --- a/docs/sparrow_run.md +++ b/docs/sparrow_run.md @@ -13,16 +13,16 @@ sparrow run [flags] ### Options ``` - --apiListeningAddress string api: The address the server is listening on (default ":8080") - -h, --help help for run - --loaderFilePath string file loader: The path to the file to read the runtime config from (default "config.yaml") - --loaderHttpRetryCount int http loader: Amount of retries trying to load the configuration (default 3) - --loaderHttpRetryDelay int http loader: The initial delay between retries in seconds (default 1) - --loaderHttpTimeout int http loader: The timeout for the http request in seconds (default 30) - --loaderHttpToken string http loader: Bearer token to authenticate the http endpoint - --loaderHttpUrl string http loader: The url where to get the remote configuration - --loaderInterval int defines the interval the loader reloads the configuration in seconds (default 300) - -l, --loaderType string defines the loader type that will load the checks configuration during the runtime. The fallback is the fileLoader (default "http") + --apiAddress string api: The address the server is listening on (default ":8080") + -h, --help help for run + --loaderFilePath string file loader: The path to the file to read the runtime config from (default "config.yaml") + --loaderHttpRetryCount int http loader: Amount of retries trying to load the configuration (default 3) + --loaderHttpRetryDelay int http loader: The initial delay between retries in seconds (default 1) + --loaderHttpTimeout int http loader: The timeout for the http request in seconds (default 30) + --loaderHttpToken string http loader: Bearer token to authenticate the http endpoint + --loaderHttpUrl string http loader: The url where to get the remote configuration + --loaderInterval int defines the interval the loader reloads the configuration in seconds (default 300) + -l, --loaderType string defines the loader type that will load the checks configuration during the runtime. The fallback is the fileLoader (default "http") ``` ### Options inherited from parent commands @@ -35,4 +35,4 @@ sparrow run [flags] * [sparrow](sparrow.md) - Sparrow, the infrastructure monitoring agent -###### Auto generated by spf13/cobra on 29-Nov-2023 +###### Auto generated by spf13/cobra on 6-Dec-2023 diff --git a/go.mod b/go.mod index 38bc5df6..5a666714 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/stretchr/testify v1.8.4 + golang.org/x/sync v0.3.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 7f66ce71..7b78dc7e 100644 --- a/go.sum +++ b/go.sum @@ -316,6 +316,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/checks/checks.go b/pkg/checks/checks.go index 942eefe0..e8f05fb5 100644 --- a/pkg/checks/checks.go +++ b/pkg/checks/checks.go @@ -31,7 +31,8 @@ import ( // The key is the name of the Check // The name needs to map the configuration item key var RegisteredChecks = map[string]func() Check{ - "health": NewHealthCheck, + "health": NewHealthCheck, + "latency": NewLatencyCheck, } //go:generate moq -out checks_moq.go . Check diff --git a/pkg/checks/health.go b/pkg/checks/health.go index edcbd157..0bb866a6 100644 --- a/pkg/checks/health.go +++ b/pkg/checks/health.go @@ -131,7 +131,6 @@ func (h *Health) RegisterHandler(ctx context.Context, router *api.RoutingTree) { _, err := w.Write([]byte("ok")) if err != nil { log.Error("Could not write response", "error", err.Error()) - // TODO: Discuss what should happen } }) } diff --git a/pkg/checks/latency.go b/pkg/checks/latency.go new file mode 100644 index 00000000..4b56ab03 --- /dev/null +++ b/pkg/checks/latency.go @@ -0,0 +1,177 @@ +package checks + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/mitchellh/mapstructure" + "golang.org/x/sync/errgroup" + + "github.com/caas-team/sparrow/internal/helper" + "github.com/caas-team/sparrow/internal/logger" + "github.com/caas-team/sparrow/pkg/api" +) + +var _ Check = (*Latency)(nil) + +func NewLatencyCheck() Check { + return &Latency{ + mu: sync.Mutex{}, + cfg: LatencyConfig{}, + c: nil, + done: make(chan bool, 1), + } +} + +type Latency struct { + cfg LatencyConfig + mu sync.Mutex + c chan<- Result + done chan bool +} + +type LatencyConfig struct { + Targets []string + Interval time.Duration + Timeout time.Duration + Retry helper.RetryConfig +} + +type LatencyResult struct { + Code int `json:"code"` + Error *string `json:"error"` + Total int64 `json:"total"` +} + +func (l *Latency) Run(ctx context.Context) error { + log := logger.FromContext(ctx).WithGroup("Latency") + log.Info(l.cfg.Interval.String()) + for { + select { + case <-ctx.Done(): + log.Error("context canceled", "err", ctx.Err()) + return ctx.Err() + case <-l.done: + return nil + case <-time.After(l.cfg.Interval): + results, err := l.check(ctx) + errval := "" + if err != nil { + errval = err.Error() + } + checkResult := Result{ + Data: results, + Err: errval, + Timestamp: time.Now(), + } + + l.c <- checkResult + } + } +} + +func (l *Latency) Startup(ctx context.Context, cResult chan<- Result) error { + log := logger.FromContext(ctx).WithGroup("latency") + log.Debug("Starting latency check") + + l.c = cResult + return nil +} + +func (l *Latency) Shutdown(_ context.Context) error { + l.done <- true + close(l.done) + + return nil +} + +func (l *Latency) SetConfig(_ context.Context, config any) error { + var c LatencyConfig + err := mapstructure.Decode(config, &c) + if err != nil { + return ErrInvalidConfig + } + c.Interval = time.Second * c.Interval + c.Retry.Delay = time.Second * c.Retry.Delay + l.mu.Lock() + defer l.mu.Unlock() + l.cfg = c + + return nil +} + +func (l *Latency) Schema() (*openapi3.SchemaRef, error) { + return OpenapiFromPerfData(make(map[string]LatencyResult)) +} + +func (l *Latency) RegisterHandler(_ context.Context, router *api.RoutingTree) { + router.Add(http.MethodGet, "v1alpha1/latency", l.Handler) +} + +func (l *Latency) DeregisterHandler(_ context.Context, router *api.RoutingTree) { + router.Remove(http.MethodGet, "v1alpha1/latency") +} + +func (l *Latency) Handler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (l *Latency) check(ctx context.Context) (map[string]LatencyResult, error) { + log := logger.FromContext(ctx).WithGroup("check") + log.Debug("Checking latency") + + var resultMutex sync.Mutex + results := map[string]LatencyResult{} + + wg, ctx := errgroup.WithContext(ctx) + for _, e := range l.cfg.Targets { + wg.Go(func(ctx context.Context, e string) func() error { + return func() error { + cl := http.Client{ + Timeout: l.cfg.Timeout * time.Second, + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, e, http.NoBody) + if err != nil { + log.Error("Error while creating request", "error", err) + return err + } + + var result LatencyResult + + req = req.WithContext(ctx) + + helper.Retry(func(ctx context.Context) error { + start := time.Now() + response, err := cl.Do(req) //nolint:bodyclose // Tom has refactored this function + if err != nil { + errval := err.Error() + result.Error = &errval + log.Error("Error while checking latency", "error", err) + } else { + result.Code = response.StatusCode + } + end := time.Now() + + result.Total = end.Sub(start).Milliseconds() + + resultMutex.Lock() + defer resultMutex.Unlock() + results[e] = result + + return err + }, l.cfg.Retry)(ctx) //nolint: errcheck //ignore return value, since we set it in the closure + return nil + } + }(ctx, e)) + } + + if err := wg.Wait(); err != nil { + log.Error("Error while checking latency", "error", err) + return nil, err + } + + return results, nil +} diff --git a/pkg/checks/latency_test.go b/pkg/checks/latency_test.go new file mode 100644 index 00000000..0a6f29fa --- /dev/null +++ b/pkg/checks/latency_test.go @@ -0,0 +1,237 @@ +package checks + +import ( + "context" + "net/http" + "net/http/httptest" + "reflect" + "sync" + "testing" + "time" + + "github.com/caas-team/sparrow/pkg/api" + "github.com/jarcoal/httpmock" +) + +func stringPointer(s string) *string { + return &s +} + +func TestLatency_check(t *testing.T) { + httpmock.Activate() + defer httpmock.Deactivate() + + httpmock.RegisterResponder(http.MethodGet, "http://success.com", httpmock.NewStringResponder(200, "ok")) + httpmock.RegisterResponder(http.MethodGet, "http://fail.com", httpmock.NewStringResponder(500, "fail")) + httpmock.RegisterResponder(http.MethodGet, "http://timeout.com", httpmock.NewErrorResponder(context.DeadlineExceeded)) + + cResult := make(chan Result, 1) + c := Latency{ + cfg: LatencyConfig{}, + mu: sync.Mutex{}, + c: cResult, + done: make(chan bool, 1), + } + results := make(chan Result, 1) + _ = c.Startup(context.Background(), results) + + _ = c.SetConfig(context.Background(), LatencyConfig{ + Targets: []string{"http://success.com", "http://fail.com", "http://timeout.com"}, + Interval: time.Second * 120, + Timeout: time.Second * 1, + }) + defer func(c *Latency, _ context.Context) { + _ = c.Shutdown(context.Background()) + }(&c, context.Background()) + + data, err := c.check(context.Background()) + if err != nil { + t.Errorf("Latency.check() error = %v", err) + } + + wantData := map[string]LatencyResult{ + "http://success.com": { + Code: 200, + Error: nil, + Total: 0, + }, + "http://fail.com": { + Code: 500, + Error: nil, + Total: 0, + }, + "http://timeout.com": { + Code: 0, + Error: stringPointer("Get \"http://timeout.com\": context deadline exceeded"), + Total: 0, + }, + } + + for k, v := range wantData { + if v.Code != data[k].Code { + t.Errorf("Latency.Run() = %v, want %v", data[k].Code, v.Code) + } + if v.Total != data[k].Total { + t.Errorf("Latency.Run() = %v, want %v", data[k].Total, v.Total) + } + if v.Error != nil && data[k].Error != nil { + if *v.Error != *data[k].Error { + t.Errorf("Latency.Run() = %v, want %v", *data[k].Error, *v.Error) + } + } + } +} + +func TestLatency_Run(t *testing.T) { + httpmock.Activate() + defer httpmock.Deactivate() + + httpmock.RegisterResponder(http.MethodGet, "http://success.com", httpmock.NewStringResponder(200, "ok")) + httpmock.RegisterResponder(http.MethodGet, "http://fail.com", httpmock.NewStringResponder(500, "fail")) + httpmock.RegisterResponder(http.MethodGet, "http://timeout.com", httpmock.NewErrorResponder(context.DeadlineExceeded)) + + c := NewLatencyCheck() + results := make(chan Result, 1) + _ = c.Startup(context.Background(), results) + + _ = c.SetConfig(context.Background(), LatencyConfig{ + Targets: []string{"http://success.com", "http://fail.com", "http://timeout.com"}, + Interval: time.Second * 120, + Timeout: time.Second * 1, + }) + go func() { + _ = c.Run(context.Background()) + }() + defer func(c Check, ctx context.Context) { + _ = c.Shutdown(ctx) + }(c, context.Background()) + + result := <-results + wantResult := Result{ + Timestamp: result.Timestamp, + Err: "", + Data: map[string]LatencyResult{ + "http://success.com": { + Code: 200, + Error: nil, + Total: 0, + }, + "http://fail.com": { + Code: 500, + Error: nil, + Total: 0, + }, + "http://timeout.com": { + Code: 0, + Error: stringPointer("Get \"http://timeout.com\": context deadline exceeded"), + Total: 0, + }, + }, + } + + if wantResult.Timestamp != result.Timestamp { + t.Errorf("Latency.Run() = %v, want %v", result.Timestamp, wantResult.Timestamp) + } + if wantResult.Err != result.Err { + t.Errorf("Latency.Run() = %v, want %v", result.Err, wantResult.Err) + } + wantData := wantResult.Data.(map[string]LatencyResult) + data := result.Data.(map[string]LatencyResult) + + for k, v := range wantData { + if v.Code != data[k].Code { + t.Errorf("Latency.Run() = %v, want %v", data[k].Code, v.Code) + } + if v.Total != data[k].Total { + t.Errorf("Latency.Run() = %v, want %v", data[k].Total, v.Total) + } + if v.Error != nil && data[k].Error != nil { + if *v.Error != *data[k].Error { + t.Errorf("Latency.Run() = %v, want %v", *data[k].Error, *v.Error) + } + } + } +} + +func TestLatency_Startup(t *testing.T) { + c := Latency{} + + if err := c.Startup(context.Background(), make(chan<- Result, 1)); err != nil { + t.Errorf("Startup() error = %v", err) + } +} + +func TestLatency_Shutdown(t *testing.T) { + cDone := make(chan bool, 1) + c := Latency{ + done: cDone, + } + err := c.Shutdown(context.Background()) + if err != nil { + t.Errorf("Shutdown() error = %v", err) + } + + if !<-cDone { + t.Error("Shutdown() should be ok") + } +} + +func TestLatency_SetConfig(t *testing.T) { + c := Latency{} + wantCfg := LatencyConfig{ + Targets: []string{"http://localhost:9090"}, + } + + err := c.SetConfig(context.Background(), wantCfg) + if err != nil { + t.Errorf("SetConfig() error = %v", err) + } + if !reflect.DeepEqual(c.cfg, wantCfg) { + t.Errorf("SetConfig() = %v, want %v", c.cfg, wantCfg) + } +} + +func TestLatency_RegisterHandler(t *testing.T) { + c := Latency{} + + rt := api.NewRoutingTree() + c.RegisterHandler(context.Background(), rt) + + h, ok := rt.Get("GET", "v1alpha1/latency") + + if !ok { + t.Error("RegisterHandler() should be ok") + } + if h == nil { + t.Error("RegisterHandler() should not be nil") + } + c.DeregisterHandler(context.Background(), rt) + h, ok = rt.Get("GET", "v1alpha1/latency") + + if ok { + t.Error("DeregisterHandler() should not be ok") + } + + if h != nil { + t.Error("DeregisterHandler() should be nil") + } +} + +func TestLatency_Handler(t *testing.T) { + c := Latency{} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/v1alpha1/latency", http.NoBody) + + c.Handler(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("Handler() should be ok, got %d", rec.Code) + } +} + +func TestNewLatencyCheck(t *testing.T) { + c := NewLatencyCheck() + if c == nil { + t.Error("NewLatencyCheck() should not be nil") + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index a4616073..50f9fb15 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -63,7 +63,7 @@ func NewConfig() *Config { } } -func (c *Config) SetApiListeningAddress(address string) { +func (c *Config) SetApiAddress(address string) { c.Api.ListeningAddress = address } diff --git a/pkg/config/file_test.go b/pkg/config/file_test.go index 74282532..bee4f547 100644 --- a/pkg/config/file_test.go +++ b/pkg/config/file_test.go @@ -25,7 +25,7 @@ import ( ) func TestNewFileLoader(t *testing.T) { - l := NewFileLoader(&Config{Loader: LoaderConfig{file: FileLoaderConfig{path: "config.yaml"}}}, make(chan<- map[string]any)) + l := NewFileLoader(&Config{Loader: LoaderConfig{file: FileLoaderConfig{path: "config.yaml"}}}, make(chan<- map[string]any, 1)) if l.path != "config.yaml" { t.Errorf("Expected path to be config.yaml, got %s", l.path) @@ -53,7 +53,7 @@ func TestFileLoader_Run(t *testing.T) { args args want want }{ - {name: "Loads config from file", fields: fields{path: "testdata/config.yaml", c: make(chan map[string]any)}, args: func() args { + {name: "Loads config from file", fields: fields{path: "testdata/config.yaml", c: make(chan map[string]any, 1)}, args: func() args { ctx, cancel := context.WithCancel(context.Background()) return args{ctx: &ctx, cancel: &cancel} }(), want: want{cfg: map[string]any{"testCheck1": map[string]any{"enabled": true}}}}, diff --git a/pkg/config/flags.go b/pkg/config/flags.go index f7b1031f..45eea6ac 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -19,7 +19,7 @@ package config type RunFlagsNameMapping struct { - ApiListeningAddress string + ApiAddress string LoaderType string LoaderInterval string diff --git a/pkg/config/http_test.go b/pkg/config/http_test.go index 2933aa9c..ffa9b761 100644 --- a/pkg/config/http_test.go +++ b/pkg/config/http_test.go @@ -143,7 +143,7 @@ func TestHttpLoader_GetRuntimeConfig(t *testing.T) { gl := &HttpLoader{ cfg: tt.cfg, - cCfgChecks: make(chan<- map[string]any), + cCfgChecks: make(chan<- map[string]any, 1), } gl.cfg.Loader.http.url = endpoint diff --git a/pkg/sparrow/api.go b/pkg/sparrow/api.go index 76ec8a9a..58daf839 100644 --- a/pkg/sparrow/api.go +++ b/pkg/sparrow/api.go @@ -64,8 +64,8 @@ func (s *Sparrow) register(ctx context.Context) { // // Blocks until context is done func (s *Sparrow) api(ctx context.Context) error { - log := logger.FromContext(ctx) - cErr := make(chan error) + log := logger.FromContext(ctx).WithGroup("api") + cErr := make(chan error, 1) s.register(ctx) server := http.Server{Addr: s.cfg.Api.ListeningAddress, Handler: s.router, ReadHeaderTimeout: readHeaderTimeout} @@ -73,8 +73,9 @@ func (s *Sparrow) api(ctx context.Context) error { // run http server in goroutine go func(cErr chan error) { defer close(cErr) + log.Info("serving api", "addr", s.cfg.Api.ListeningAddress) if err := server.ListenAndServe(); err != nil { - log.Error("failed to serve api", "error", err) + log.Error("Failed to serve api", "error", err) cErr <- err } }(cErr) @@ -86,15 +87,14 @@ func (s *Sparrow) api(ctx context.Context) error { defer cancel() err := server.Shutdown(shutdownCtx) if err != nil { - log.Error("failed to shutdown api", "error", err) - // TODO: panic? return error? + log.Error("Failed to shutdown api server", "error", err) } - log.Error("api context canceled", "error", ctx.Err()) + log.Error("Api context canceled", "error", ctx.Err()) return fmt.Errorf("%w: %w", ErrApiContext, ctx.Err()) } case err := <-cErr: if errors.Is(err, http.ErrServerClosed) || err == nil { - log.Info("api server closed") + log.Info("Api server closed") return nil } log.Error("failed to serve api", "error", err) @@ -163,8 +163,7 @@ func (s *Sparrow) getCheckMetrics(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, err := w.Write([]byte(http.StatusText(http.StatusBadRequest))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } @@ -173,8 +172,7 @@ func (s *Sparrow) getCheckMetrics(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, err := w.Write([]byte(http.StatusText(http.StatusNotFound))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } @@ -185,8 +183,7 @@ func (s *Sparrow) getCheckMetrics(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } @@ -201,8 +198,7 @@ func (s *Sparrow) getOpenapi(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } @@ -225,8 +221,7 @@ func (s *Sparrow) getOpenapi(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, err = w.Write([]byte(http.StatusText(http.StatusInternalServerError))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } @@ -245,8 +240,7 @@ func (s *Sparrow) handleChecks(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, err := w.Write([]byte(http.StatusText(http.StatusNotFound))) if err != nil { - log.Error("failed to write response", "error", err) - // TODO: anything else to do here? + log.Error("Failed to write response", "error", err) } return } diff --git a/pkg/sparrow/run.go b/pkg/sparrow/run.go index 39d76672..eaa4615d 100644 --- a/pkg/sparrow/run.go +++ b/pkg/sparrow/run.go @@ -20,6 +20,7 @@ package sparrow import ( "context" + "fmt" "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/api" @@ -32,7 +33,8 @@ import ( type Sparrow struct { // TODO refactor this struct to be less convoluted // split up responsibilities more clearly - db db.DB + db db.DB + // the existing checks checks map[string]checks.Check resultFanIn map[string]chan checks.Result @@ -48,15 +50,14 @@ type Sparrow struct { // New creates a new sparrow from a given configfile func New(cfg *config.Config) *Sparrow { - // TODO read this from config file sparrow := &Sparrow{ router: chi.NewRouter(), routingTree: api.NewRoutingTree(), checks: make(map[string]checks.Check), - cResult: make(chan checks.ResultDTO), + cResult: make(chan checks.ResultDTO, 1), resultFanIn: make(map[string]chan checks.Result), cfg: cfg, - cCfgChecks: make(chan map[string]any), + cCfgChecks: make(chan map[string]any, 1), db: db.NewInMemory(), } @@ -69,16 +70,13 @@ func New(cfg *config.Config) *Sparrow { func (s *Sparrow) Run(ctx context.Context) error { ctx, cancel := logger.NewContextWithLogger(ctx, "sparrow") defer cancel() - // TODO Setup before checks run - // setup http server // Start the runtime configuration loader go s.loader.Run(ctx) go func() { err := s.api(ctx) if err != nil { - panic(err) - // TODO: discuss what should happen - probably panic? + panic(fmt.Sprintf("Failed to start api: %v", err)) } }() @@ -100,7 +98,8 @@ func (s *Sparrow) Run(ctx context.Context) error { } } -// ReconcileChecks registers new Checks, unregisters removed Checks & resets the Configs of Checks +// ReconcileChecks registers new Checks, unregisters removed Checks, +// resets the Configs of Checks and starts running the checks func (s *Sparrow) ReconcileChecks(ctx context.Context) { for name, checkCfg := range s.cfg.Checks { name := name @@ -123,7 +122,7 @@ func (s *Sparrow) ReconcileChecks(ctx context.Context) { s.checks[name] = check // Create a fan in a channel for the check - checkChan := make(chan checks.Result) + checkChan := make(chan checks.Result, 1) s.resultFanIn[name] = checkChan err := check.SetConfig(ctx, checkCfg) @@ -135,7 +134,6 @@ func (s *Sparrow) ReconcileChecks(ctx context.Context) { if err != nil { log.ErrorContext(ctx, "Failed to startup check", "name", name, "error", err) close(checkChan) - // TODO discuss whether this should return an error instead? continue } check.RegisterHandler(ctx, s.routingTree) @@ -143,23 +141,21 @@ func (s *Sparrow) ReconcileChecks(ctx context.Context) { err := check.Run(ctx) if err != nil { log.ErrorContext(ctx, "Failed to run check", "name", name, "error", err) - // TODO: anything else to do here? } }() } for existingCheckName, existingCheck := range s.checks { - // Check has been removed from config; shutdown and remove log := logger.FromContext(ctx).With("checkName", existingCheckName) if _, ok := s.cfg.Checks[existingCheckName]; ok { continue } + // Check has been removed from config; shutdown and remove existingCheck.DeregisterHandler(ctx, s.routingTree) err := existingCheck.Shutdown(ctx) if err != nil { log.ErrorContext(ctx, "Failed to shutdown check", "error", err) - // TODO: discuss what should happen } if c, ok := s.resultFanIn[existingCheckName]; ok { // close fan in the channel if it exists diff --git a/pkg/sparrow/run_test.go b/pkg/sparrow/run_test.go index 53f19ae9..72e98091 100644 --- a/pkg/sparrow/run_test.go +++ b/pkg/sparrow/run_test.go @@ -86,7 +86,7 @@ func TestSparrow_ReconcileChecks(t *testing.T) { fields: fields{ checks: map[string]checks.Check{}, cfg: &config.Config{}, - cCfgChecks: make(chan map[string]any), + cCfgChecks: make(chan map[string]any, 1), resultFanIn: make(map[string]chan checks.Result), }, newChecksConfig: map[string]any{ @@ -100,7 +100,7 @@ func TestSparrow_ReconcileChecks(t *testing.T) { "alpha": checks.RegisteredChecks["alpha"](), }, cfg: &config.Config{}, - cCfgChecks: make(chan map[string]any), + cCfgChecks: make(chan map[string]any, 1), resultFanIn: make(map[string]chan checks.Result), }, newChecksConfig: map[string]any{ @@ -115,7 +115,7 @@ func TestSparrow_ReconcileChecks(t *testing.T) { "alpha": checks.RegisteredChecks["alpha"](), }, cfg: &config.Config{}, - cCfgChecks: make(chan map[string]any), + cCfgChecks: make(chan map[string]any, 1), resultFanIn: make(map[string]chan checks.Result), }, newChecksConfig: map[string]any{}, @@ -128,7 +128,7 @@ func TestSparrow_ReconcileChecks(t *testing.T) { "gamma": checks.RegisteredChecks["alpha"](), }, cfg: &config.Config{}, - cCfgChecks: make(chan map[string]any), + cCfgChecks: make(chan map[string]any, 1), resultFanIn: make(map[string]chan checks.Result), }, newChecksConfig: map[string]any{ @@ -163,8 +163,8 @@ func TestSparrow_ReconcileChecks(t *testing.T) { } func Test_fanInResults(t *testing.T) { - checkChan := make(chan checks.Result) - cResult := make(chan checks.ResultDTO) + checkChan := make(chan checks.Result, 1) + cResult := make(chan checks.ResultDTO, 1) name := "check" go fanInResults(checkChan, cResult, name)