Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aa83ee274 | ||
|
|
1e431c797a | ||
|
|
143a093e20 | ||
|
|
1eba430797 | ||
|
|
6299b630ce | ||
|
|
1fcc6c0cc0 | ||
|
|
408a46f2af | ||
|
|
3269e96f49 | ||
|
|
08742e4af3 | ||
|
|
2a623a59d3 | ||
|
|
3d1b4e566d | ||
|
|
6cbc59b0e8 | ||
|
|
1a7aeb5b35 | ||
|
|
228cd4d1fb | ||
|
|
bdad56e205 | ||
|
|
911902353f | ||
|
|
f76be6df92 | ||
|
|
dd6c4142fb | ||
|
|
1e82d2f07d | ||
|
|
3c246f0c69 | ||
|
|
76111ee133 | ||
|
|
d3a82244a1 | ||
|
|
807599c05e | ||
|
|
de7256e671 | ||
|
|
c6515c4b1c | ||
|
|
522b958d0f | ||
|
|
16366e169e | ||
|
|
ea3ae52f1e | ||
|
|
5a16151bba | ||
|
|
802ad7ff8f | ||
|
|
619b69f480 | ||
|
|
87e029f555 | ||
|
|
71c4d3ade1 | ||
|
|
315f9b7792 | ||
|
|
bde30b2efb | ||
|
|
88cb92745b | ||
|
|
744d63abac | ||
|
|
c1cdf50851 | ||
|
|
2bd268670f | ||
|
|
e88bfa8518 | ||
|
|
0fa3c5d114 | ||
|
|
0903d28b56 | ||
|
|
b50f3f3646 | ||
|
|
1b0dfdd09d | ||
|
|
6d3468d81a | ||
|
|
200d007eca | ||
|
|
24c3a84db9 | ||
|
|
0402bdb774 | ||
|
|
c7af44bcb0 | ||
|
|
494a8594cc | ||
|
|
81dd84e5f2 | ||
|
|
aa7f8131cd | ||
|
|
4dea597726 | ||
|
|
8df77b09ed | ||
|
|
d2b274e609 | ||
|
|
15c81f93d2 | ||
|
|
05565e3d0a | ||
|
|
8fbfba2163 | ||
|
|
0e9df7f00f | ||
|
|
7d570ce148 | ||
|
|
67941865db | ||
|
|
5c5a954b68 | ||
|
|
5f69351b6b | ||
|
|
34313bec7e | ||
|
|
640e455d33 | ||
|
|
2be6a1e5f3 | ||
|
|
225e6ac7ae | ||
|
|
87d9722621 | ||
|
|
fd17dcd204 | ||
|
|
2f6b8f23f7 | ||
|
|
0a37a61619 | ||
|
|
70f9f8738c | ||
|
|
a725e7e770 | ||
|
|
fc29c45e5f | ||
|
|
6a8c308af7 | ||
|
|
0e33556775 | ||
|
|
6bb65f4eec | ||
|
|
9142ff837c | ||
|
|
11eb7fb02e | ||
|
|
271b836160 | ||
|
|
c42f3ef787 | ||
|
|
c7a774b213 | ||
|
|
36207490b2 | ||
|
|
5eebe6d9cc | ||
|
|
ecda4a9987 | ||
|
|
162dd9bc7c | ||
|
|
60b0f3fa29 | ||
|
|
0cf7621162 | ||
|
|
8d27864aaa | ||
|
|
3bf880a199 | ||
|
|
719f684982 | ||
|
|
a3df822df3 | ||
|
|
d62a6c5054 | ||
|
|
fcf8b5d86f | ||
|
|
5bbd240dd9 | ||
|
|
7e163c3fcf | ||
|
|
1dd0ecf10b | ||
|
|
3350e81443 | ||
|
|
c3e1835dd6 | ||
|
|
2a45a151da | ||
|
|
74cde8ae8d | ||
|
|
669877baf4 | ||
|
|
447e140479 | ||
|
|
6908199716 | ||
|
|
b12f652553 | ||
|
|
83edca6e80 | ||
|
|
636688b43e | ||
|
|
4fdb55d632 | ||
|
|
a05daeda2e | ||
|
|
0bd0c1fd15 | ||
|
|
eb3ca71c72 | ||
|
|
37325cd78a | ||
|
|
f6e7e346b6 | ||
|
|
b5e742acde |
@@ -4,4 +4,5 @@ Dockerfile
|
||||
.idea
|
||||
.git
|
||||
web/app
|
||||
*.db
|
||||
*.db
|
||||
testdata
|
||||
@@ -1,6 +1,6 @@
|
||||
storage:
|
||||
type: postgres
|
||||
path: "postgres://username:password@postgres:5432/gatus?sslmode=disable"
|
||||
path: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable"
|
||||
|
||||
endpoints:
|
||||
- name: back-end
|
||||
|
||||
@@ -18,6 +18,10 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- POSTGRES_USER=username
|
||||
- POSTGRES_PASSWORD=password
|
||||
- POSTGRES_DB=gatus
|
||||
volumes:
|
||||
- ./config:/config
|
||||
networks:
|
||||
|
||||
BIN
.github/assets/gitlab-alerts.png
vendored
Normal file
BIN
.github/assets/gitlab-alerts.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
BIN
.github/assets/gotify-alerts.png
vendored
Normal file
BIN
.github/assets/gotify-alerts.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -4,12 +4,10 @@ updates:
|
||||
directory: "/"
|
||||
labels: ["dependencies"]
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "wednesday"
|
||||
interval: "daily"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 1
|
||||
labels: ["dependencies"]
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "wednesday"
|
||||
interval: "daily"
|
||||
|
||||
4
.github/workflows/benchmark.yml
vendored
4
.github/workflows/benchmark.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
||||
ref: "${{ github.event.inputs.ref || 'master' }}"
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Benchmark
|
||||
run: go test -bench=. ./storage/store
|
||||
|
||||
10
.github/workflows/publish-experimental.yml
vendored
10
.github/workflows/publish-experimental.yml
vendored
@@ -5,20 +5,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
pull: true
|
||||
|
||||
14
.github/workflows/publish-latest-to-ghcr.yml
vendored
14
.github/workflows/publish-latest-to-ghcr.yml
vendored
@@ -5,32 +5,32 @@ on:
|
||||
branches: [master]
|
||||
types: [completed]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }}::${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
publish-latest-to-ghcr:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
|
||||
14
.github/workflows/publish-latest.yml
vendored
14
.github/workflows/publish-latest.yml
vendored
@@ -5,28 +5,28 @@ on:
|
||||
branches: [master]
|
||||
types: [completed]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
group: ${{ github.event.workflow_run.head_repository.full_name }}::${{ github.event.workflow_run.head_branch }}::${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
publish-latest:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
|
||||
10
.github/workflows/publish-release-to-ghcr.yml
vendored
10
.github/workflows/publish-release-to-ghcr.yml
vendored
@@ -10,23 +10,23 @@ jobs:
|
||||
packages: write
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Get the release
|
||||
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
|
||||
10
.github/workflows/publish-release.yml
vendored
10
.github/workflows/publish-release.yml
vendored
@@ -8,22 +8,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Get image repository
|
||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||
- name: Get the release
|
||||
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
pull: true
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -14,12 +14,12 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/checkout@v3
|
||||
go-version: 1.21
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build binary to make sure it works
|
||||
run: go build
|
||||
- name: Test
|
||||
@@ -28,6 +28,6 @@ jobs:
|
||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
||||
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race -coverprofile=coverage.txt -covermode=atomic
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v3.1.1
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM golang:alpine as builder
|
||||
RUN apk --update add ca-certificates
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
RUN go mod tidy
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gatus .
|
||||
|
||||
# Run Tests inside docker image if you don't have a configured go environment
|
||||
|
||||
9
Makefile
9
Makefile
@@ -1,17 +1,18 @@
|
||||
BINARY=gatus
|
||||
|
||||
# Because there's a folder called "test", we need to make the target "test" phony
|
||||
.PHONY: test
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go build -mod vendor -o $(BINARY) .
|
||||
go build -v -o $(BINARY) .
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
GATUS_CONFIG_PATH=./config.yaml ./$(BINARY)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm $(BINARY)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./... -cover
|
||||
|
||||
|
||||
426
README.md
426
README.md
@@ -38,6 +38,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
|
||||
|
||||
## Table of Contents
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Why Gatus?](#why-gatus)
|
||||
- [Features](#features)
|
||||
- [Usage](#usage)
|
||||
@@ -51,7 +52,9 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||
- [Configuring Email alerts](#configuring-email-alerts)
|
||||
- [Configuring GitHub alerts](#configuring-github-alerts)
|
||||
- [Configuring GitLab alerts](#configuring-gitlab-alerts)
|
||||
- [Configuring Google Chat alerts](#configuring-google-chat-alerts)
|
||||
- [Configuring Gotify alerts](#configuring-gotify-alerts)
|
||||
- [Configuring Matrix alerts](#configuring-matrix-alerts)
|
||||
- [Configuring Mattermost alerts](#configuring-mattermost-alerts)
|
||||
- [Configuring Messagebird alerts](#configuring-messagebird-alerts)
|
||||
@@ -63,13 +66,16 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Configuring Teams alerts](#configuring-teams-alerts)
|
||||
- [Configuring Telegram alerts](#configuring-telegram-alerts)
|
||||
- [Configuring Twilio alerts](#configuring-twilio-alerts)
|
||||
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
||||
- [Configuring custom alerts](#configuring-custom-alerts)
|
||||
- [Setting a default alert](#setting-a-default-alert)
|
||||
- [Maintenance](#maintenance)
|
||||
- [Security](#security)
|
||||
- [Basic](#basic)
|
||||
- [Basic Authentication](#basic-authentication)
|
||||
- [OIDC](#oidc)
|
||||
- [TLS Encryption](#tls-encryption)
|
||||
- [Metrics](#metrics)
|
||||
- [Connectivity](#connectivity)
|
||||
- [Remote instances (EXPERIMENTAL)](#remote-instances-experimental)
|
||||
- [Deployment](#deployment)
|
||||
- [Docker](#docker)
|
||||
@@ -84,23 +90,30 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
||||
- [Monitoring a TCP endpoint](#monitoring-a-tcp-endpoint)
|
||||
- [Monitoring a UDP endpoint](#monitoring-a-udp-endpoint)
|
||||
- [Monitoring a SCTP endpoint](#monitoring-a-sctp-endpoint)
|
||||
- [Monitoring a WebSocket endpoint](#monitoring-a-websocket-endpoint)
|
||||
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
|
||||
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
|
||||
- [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh)
|
||||
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
|
||||
- [Monitoring an endpoint using TLS](#monitoring-an-endpoint-using-tls)
|
||||
- [Monitoring domain expiration](#monitoring-domain-expiration)
|
||||
- [disable-monitoring-lock](#disable-monitoring-lock)
|
||||
- [Reloading configuration on the fly](#reloading-configuration-on-the-fly)
|
||||
- [Endpoint groups](#endpoint-groups)
|
||||
- [Exposing Gatus on a custom path](#exposing-gatus-on-a-custom-path)
|
||||
- [Exposing Gatus on a custom port](#exposing-gatus-on-a-custom-port)
|
||||
- [Configuring a startup delay](#configuring-a-startup-delay)
|
||||
- [Keeping your configuration small](#keeping-your-configuration-small)
|
||||
- [Proxy client configuration](#proxy-client-configuration)
|
||||
- [Badges](#badges)
|
||||
- [Uptime](#uptime)
|
||||
- [Health](#health)
|
||||
- [Health (Shields.io)](#health-shieldsio)
|
||||
- [Response time](#response-time)
|
||||
- [How to change the color thresholds of the response time badge](#how-to-change-the-color-thresholds-of-the-response-time-badge)
|
||||
- [API](#api)
|
||||
- [Installing as binary](#installing-as-binary)
|
||||
- [High level design overview](#high-level-design-overview)
|
||||
- [Sponsors](#sponsors)
|
||||
|
||||
|
||||
## Why Gatus?
|
||||
@@ -120,9 +133,9 @@ if no traffic makes it to your applications. This puts you in a situation where
|
||||
that will notify you about the degradation of your services rather than you reassuring them that you're working on
|
||||
fixing the issue before they even know about it.
|
||||
|
||||
|
||||
## Features
|
||||
The main features of Gatus are:
|
||||
|
||||
- **Highly flexible health check conditions**: While checking the response status may be enough for some use cases, Gatus goes much further and allows you to add conditions on the response time, the response body and even the IP address.
|
||||
- **Ability to use Gatus for user acceptance tests**: Thanks to the point above, you can leverage this application to create automated user acceptance tests.
|
||||
- **Very easy to configure**: Not only is the configuration designed to be as readable as possible, it's also extremely easy to add a new service or a new endpoint to monitor.
|
||||
@@ -134,7 +147,6 @@ The main features of Gatus are:
|
||||
|
||||

|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
<details>
|
||||
@@ -185,6 +197,8 @@ subdirectories are merged like so:
|
||||
- To clarify, this also means that you could not define `alerting.slack.webhook-url` in two files with different values. All files are merged into one before they are processed. This is by design.
|
||||
|
||||
> 💡 You can also use environment variables in the configuration file (e.g. `$DOMAIN`, `${DOMAIN}`)
|
||||
>
|
||||
> See [examples/docker-compose-postgres-storage/config/config.yaml](.examples/docker-compose-postgres-storage/config/config.yaml) for an example.
|
||||
|
||||
If you want to test it locally, see [Docker](#docker).
|
||||
|
||||
@@ -209,6 +223,9 @@ If you want to test it locally, see [Docker](#docker).
|
||||
| `endpoints[].dns` | Configuration for an endpoint of type DNS. <br />See [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries). | `""` |
|
||||
| `endpoints[].dns.query-type` | Query type (e.g. MX) | `""` |
|
||||
| `endpoints[].dns.query-name` | Query name (e.g. example.com) | `""` |
|
||||
| `endpoints[].ssh` | Configuration for an endpoint of type SSH. <br />See [Monitoring an endpoint using SSH](#monitoring-an-endpoint-using-ssh). | `""` |
|
||||
| `endpoints[].ssh.username` | SSH username (e.g. example) | Required `""` |
|
||||
| `endpoints[].ssh.password` | SSH password (e.g. password) | Required `""` |
|
||||
| `endpoints[].alerts[].type` | Type of alert. <br />See [Alerting](#alerting) for all valid types. | Required `""` |
|
||||
| `endpoints[].alerts[].enabled` | Whether to enable the alert. | `true` |
|
||||
| `endpoints[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert. | `3` |
|
||||
@@ -228,6 +245,9 @@ If you want to test it locally, see [Docker](#docker).
|
||||
| `web` | Web configuration. | `{}` |
|
||||
| `web.address` | Address to listen on. | `0.0.0.0` |
|
||||
| `web.port` | Port to listen on. | `8080` |
|
||||
| `web.read-buffer-size` | Buffer size for reading requests from a connection. Also limit for the maximum header size. | `8192` |
|
||||
| `web.tls.certificate-file` | Optional public certificate file for TLS in PEM format. | `` |
|
||||
| `web.tls.private-key-file` | Optional private key file for TLS in PEM format. | `` |
|
||||
| `ui` | UI configuration. | `{}` |
|
||||
| `ui.title` | [Title of the document](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). | `Health Dashboard ǀ Gatus` |
|
||||
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
||||
@@ -330,31 +350,38 @@ See [examples/docker-compose-postgres-storage](.examples/docker-compose-postgres
|
||||
In order to support a wide range of environments, each monitored endpoint has a unique configuration for
|
||||
the client used to send the request.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------|:---------------------------------------------------------------------------|:----------------|
|
||||
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
||||
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
||||
| `client.timeout` | Duration before timing out. | `10s` |
|
||||
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
||||
| `client.oauth2` | OAuth2 client configuration. | `{}` |
|
||||
| `client.oauth2.token-url` | The token endpoint URL | required `""` |
|
||||
| `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` |
|
||||
| `client.oauth2.client-secret` | The client secret which should be used for the `Client credentials flow` | required `""` |
|
||||
| `client.oauth2.scopes[]` | A list of `scopes` which should be used for the `Client credentials flow`. | required `[""]` |
|
||||
| Parameter | Description | Default |
|
||||
| :------------------------------------- | :-------------------------------------------------------------------------- | :-------------- |
|
||||
| `client.insecure` | Whether to skip verifying the server's certificate chain and host name. | `false` |
|
||||
| `client.ignore-redirect` | Whether to ignore redirects (true) or follow them (false, default). | `false` |
|
||||
| `client.timeout` | Duration before timing out. | `10s` |
|
||||
| `client.dns-resolver` | Override the DNS resolver using the format `{proto}://{host}:{port}`. | `""` |
|
||||
| `client.oauth2` | OAuth2 client configuration. | `{}` |
|
||||
| `client.oauth2.token-url` | The token endpoint URL | required `""` |
|
||||
| `client.oauth2.client-id` | The client id which should be used for the `Client credentials flow` | required `""` |
|
||||
| `client.oauth2.client-secret` | The client secret which should be used for the `Client credentials flow` | required `""` |
|
||||
| `client.oauth2.scopes[]` | A list of `scopes` which should be used for the `Client credentials flow`. | required `[""]` |
|
||||
| `client.proxy-url` | The URL of the proxy to use for the client | `""` |
|
||||
| `client.identity-aware-proxy` | Google Identity-Aware-Proxy client configuration. | `{}` |
|
||||
| `client.identity-aware-proxy.audience` | The Identity-Aware-Proxy audience. (client-id of the IAP oauth2 credential) | required `""` |
|
||||
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
|
||||
|
||||
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
||||
in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
|
||||
|
||||
This default configuration is as follows:
|
||||
|
||||
```yaml
|
||||
client:
|
||||
insecure: false
|
||||
ignore-redirect: false
|
||||
timeout: 10s
|
||||
```
|
||||
|
||||
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
||||
|
||||
Here's an example with the client configuration under `endpoints[]`:
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: website
|
||||
@@ -368,6 +395,7 @@ endpoints:
|
||||
```
|
||||
|
||||
This example shows how you can specify a custom DNS resolver:
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: with-custom-dns-resolver
|
||||
@@ -379,6 +407,7 @@ endpoints:
|
||||
```
|
||||
|
||||
This example shows how you can use the `client.oauth2` configuration to query a backend API with `Bearer token`:
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: with-custom-oauth2
|
||||
@@ -393,6 +422,20 @@ endpoints:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
This example shows how you can use the `client.identity-aware-proxy` configuration to query a backend API with `Bearer token` using Google Identity-Aware-Proxy:
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: with-custom-iap
|
||||
url: "https://my.iap.protected.app/health"
|
||||
client:
|
||||
identity-aware-proxy:
|
||||
audience: "XXXXXXXX-XXXXXXXXXXXX.apps.googleusercontent.com"
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
> 📝 Note that Gatus will use the [gcloud default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) within its environment to generate the token.
|
||||
|
||||
### Alerting
|
||||
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
|
||||
@@ -407,7 +450,9 @@ ignored.
|
||||
| `alerting.discord` | Configuration for alerts of type `discord`. <br />See [Configuring Discord alerts](#configuring-discord-alerts). | `{}` |
|
||||
| `alerting.email` | Configuration for alerts of type `email`. <br />See [Configuring Email alerts](#configuring-email-alerts). | `{}` |
|
||||
| `alerting.github` | Configuration for alerts of type `github`. <br />See [Configuring GitHub alerts](#configuring-github-alerts). | `{}` |
|
||||
| `alerting.gitlab` | Configuration for alerts of type `gitlab`. <br />See [Configuring GitLab alerts](#configuring-gitlab-alerts). | `{}` |
|
||||
| `alerting.googlechat` | Configuration for alerts of type `googlechat`. <br />See [Configuring Google Chat alerts](#configuring-google-chat-alerts). | `{}` |
|
||||
| `alerting.gotify` | Configuration for alerts of type `gotify`. <br />See [Configuring Gotify alerts](#configuring-gotify-alerts). | `{}` |
|
||||
| `alerting.matrix` | Configuration for alerts of type `matrix`. <br />See [Configuring Matrix alerts](#configuring-matrix-alerts). | `{}` |
|
||||
| `alerting.mattermost` | Configuration for alerts of type `mattermost`. <br />See [Configuring Mattermost alerts](#configuring-mattermost-alerts). | `{}` |
|
||||
| `alerting.messagebird` | Configuration for alerts of type `messagebird`. <br />See [Configuring Messagebird alerts](#configuring-messagebird-alerts). | `{}` |
|
||||
@@ -426,6 +471,7 @@ ignored.
|
||||
|:-------------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.discord` | Configuration for alerts of type `discord` | `{}` |
|
||||
| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` |
|
||||
| `alerting.discord.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` |
|
||||
| `alerting.discord.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.discord.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.discord.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
@@ -452,19 +498,20 @@ endpoints:
|
||||
|
||||
|
||||
#### Configuring Email alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
|
||||
| `alerting.email.password` | Password of the SMTP server used to send the alert | Required `""` |
|
||||
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.email.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.email.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.email.overrides[].to` | Email(s) to send the alerts to | `""` |
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:----------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.email` | Configuration for alerts of type `email` | `{}` |
|
||||
| `alerting.email.from` | Email used to send the alert | Required `""` |
|
||||
| `alerting.email.username` | Username of the SMTP server used to send the alert. If empty, uses `alerting.email.from`. | `""` |
|
||||
| `alerting.email.password` | Password of the SMTP server used to send the alert. If empty, no authentication is performed. | `""` |
|
||||
| `alerting.email.host` | Host of the mail server (e.g. `smtp.gmail.com`) | Required `""` |
|
||||
| `alerting.email.port` | Port the mail server is listening to (e.g. `587`) | Required `0` |
|
||||
| `alerting.email.to` | Email(s) to send the alerts to | Required `""` |
|
||||
| `alerting.email.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
| `alerting.email.client.insecure` | Whether to skip TLS verification | `false` |
|
||||
| `alerting.email.overrides` | List of overrides that may be prioritized over the default configuration | `[]` |
|
||||
| `alerting.email.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` |
|
||||
| `alerting.email.overrides[].to` | Email(s) to send the alerts to | `""` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
@@ -475,6 +522,8 @@ alerting:
|
||||
host: "mail.example.com"
|
||||
port: 587
|
||||
to: "recipient1@example.com,recipient2@example.com"
|
||||
client:
|
||||
insecure: false
|
||||
# You can also add group-specific to keys, which will
|
||||
# override the to key above for the specified groups
|
||||
overrides:
|
||||
@@ -546,6 +595,47 @@ endpoints:
|
||||
|
||||

|
||||
|
||||
#### Configuring GitLab alerts
|
||||
| Parameter | Description | Default |
|
||||
|:------------------------------------|:----------------------------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.gitlab` | Configuration for alerts of type `gitlab` | `{}` |
|
||||
| `alerting.gitlab.webhook-url` | GitLab alert webhook URL (e.g. `https://gitlab.com/hlidotbe/example/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json`) | Required `""` |
|
||||
| `alerting.gitlab.authorization-key` | GitLab alert authorization key. | Required `""` |
|
||||
| `alerting.gitlab.severity` | Override default severity (critical), can be one of `critical, high, medium, low, info, unknown` | `""` |
|
||||
| `alerting.gitlab.monitoring-tool` | Override the monitoring tool name (gatus) | `"gatus"` |
|
||||
| `alerting.gitlab.environment-name` | Set gitlab environment's name. Required to display alerts on a dashboard. | `""` |
|
||||
| `alerting.gitlab.service` | Override endpoint displayname | `""` |
|
||||
| `alerting.gitlab.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||
|
||||
The GitLab alerting provider creates an alert prefixed with `alert(gatus):` and suffixed with the endpoint's display
|
||||
name for each alert. If `send-on-resolved` is set to `true` on the endpoint alert, the alert will be automatically
|
||||
closed when the alert is resolved. See
|
||||
https://docs.gitlab.com/ee/operations/incident_management/integrations.html#configuration to configure the endpoint.
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
gitlab:
|
||||
webhook-url: "https://gitlab.com/hlidotbe/example/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json"
|
||||
authorization-key: "12345"
|
||||
|
||||
endpoints:
|
||||
- name: example
|
||||
url: "https://twin.sh/health"
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
- "[RESPONSE_TIME] < 75"
|
||||
alerts:
|
||||
- type: gitlab
|
||||
failure-threshold: 2
|
||||
success-threshold: 3
|
||||
send-on-resolved: true
|
||||
description: "Everything's burning AAAAAHHHHHHHHHHHHHHH"
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
#### Configuring Google Chat alerts
|
||||
| Parameter | Description | Default |
|
||||
@@ -578,6 +668,41 @@ endpoints:
|
||||
```
|
||||
|
||||
|
||||
#### Configuring Gotify alerts
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------------|:--------------------------------------------------------------------------------------------|:-----------------------|
|
||||
| `alerting.gotify` | Configuration for alerts of type `gotify` | `{}` |
|
||||
| `alerting.gotify.server-url` | Gotify server URL | Required `""` |
|
||||
| `alerting.gotify.token` | Token that is used for authentication. | Required `""` |
|
||||
| `alerting.gotify.priority` | Priority of the alert according to Gotify standarts. | `5` |
|
||||
| `alerting.gotify.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert). | N/A |
|
||||
| `alerting.gotify.title` | Title of the notification | `"Gatus: <endpoint>"` |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
gotify:
|
||||
server-url: "https://gotify.example"
|
||||
token: "**************"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: "https://twin.sh/health"
|
||||
interval: 5m
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
- "[RESPONSE_TIME] < 300"
|
||||
alerts:
|
||||
- type: gotify
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
```
|
||||
|
||||
Here's an example of what the notifications look like:
|
||||
|
||||

|
||||
|
||||
|
||||
#### Configuring Matrix alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------------|:-------------------------------------------------------------------------------------------|:-----------------------------------|
|
||||
@@ -685,6 +810,7 @@ endpoints:
|
||||
| `alerting.ntfy` | Configuration for alerts of type `ntfy` | `{}` |
|
||||
| `alerting.ntfy.topic` | Topic at which the alert will be sent | Required `""` |
|
||||
| `alerting.ntfy.url` | The URL of the target server | `https://ntfy.sh` |
|
||||
| `alerting.ntfy.token` | [Access token](https://docs.ntfy.sh/publish/#access-tokens) for restricted topics | `""` |
|
||||
| `alerting.ntfy.priority` | The priority of the alert | `3` |
|
||||
| `alerting.ntfy.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
@@ -697,6 +823,7 @@ alerting:
|
||||
ntfy:
|
||||
topic: "gatus-test-topic"
|
||||
priority: 2
|
||||
token: faketoken
|
||||
default-alert:
|
||||
failure-threshold: 3
|
||||
send-on-resolved: true
|
||||
@@ -984,6 +1111,46 @@ endpoints:
|
||||
```
|
||||
|
||||
|
||||
#### Configuring AWS SES alerts
|
||||
| Parameter | Description | Default |
|
||||
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
| `alerting.aws-ses` | Settings for alerts of type `aws-ses` | `{}` |
|
||||
| `alerting.aws-ses.access-key-id` | AWS Access Key ID | Optional `""` |
|
||||
| `alerting.aws-ses.secret-access-key` | AWS Secret Access Key | Optional `""` |
|
||||
| `alerting.aws-ses.region` | AWS Region | Required `""` |
|
||||
| `alerting.aws-ses.from` | The Email address to send the emails from (should be registered in SES) | Required `""` |
|
||||
| `alerting.aws-ses.to` | Comma separated list of email address to notify | Required `""` |
|
||||
| `alerting.aws-ses.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
aws-ses:
|
||||
access-key-id: "..."
|
||||
secret-access-key: "..."
|
||||
region: "us-east-1"
|
||||
from: "status@example.com"
|
||||
to: "user@example.com"
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
interval: 30s
|
||||
url: "https://twin.sh/health"
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].status == UP"
|
||||
- "[RESPONSE_TIME] < 300"
|
||||
alerts:
|
||||
- type: aws-ses
|
||||
failure-threshold: 5
|
||||
send-on-resolved: true
|
||||
description: "healthcheck failed"
|
||||
```
|
||||
|
||||
If the `access-key-id` and `secret-access-key` are not defined Gatus will fall back to IAM authentication.
|
||||
|
||||
Make sure you have the ability to use `ses:SendEmail`.
|
||||
|
||||
|
||||
#### Configuring custom alerts
|
||||
| Parameter | Description | Default |
|
||||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||
@@ -1053,13 +1220,13 @@ As a result, the `[ALERT_TRIGGERED_OR_RESOLVED]` in the body of first example of
|
||||
|
||||
|
||||
#### Setting a default alert
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------------|:------------------------------------------------------------------------------|:--------|
|
||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
||||
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
|
||||
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------------------|:------------------------------------------------------------------------------|:--------|
|
||||
| `alerting.*.default-alert.enabled` | Whether to enable the alert | N/A |
|
||||
| `alerting.*.default-alert.failure-threshold` | Number of failures in a row needed before triggering the alert | N/A |
|
||||
| `alerting.*.default-alert.success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.send-on-resolved` | Whether to send a notification once a triggered alert is marked as resolved | N/A |
|
||||
| `alerting.*.default-alert.description` | Description of the alert. Will be included in the alert sent | N/A |
|
||||
|
||||
> ⚠ You must still specify the `type` of the alert in the endpoint configuration even if you set the default alert of a provider.
|
||||
|
||||
@@ -1175,14 +1342,14 @@ maintenance:
|
||||
|
||||
|
||||
### Security
|
||||
| Parameter | Description | Default |
|
||||
|:---------------------------------|:-----------------------------|:--------------|
|
||||
| `security` | Security configuration | `{}` |
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------|:-----------------------------|:--------|
|
||||
| `security` | Security configuration | `{}` |
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
| `security.oidc` | OpenID Connect configuration | `{}` |
|
||||
|
||||
|
||||
#### Basic
|
||||
#### Basic Authentication
|
||||
| Parameter | Description | Default |
|
||||
|:----------------------------------------|:-----------------------------------------------------------------------------------|:--------------|
|
||||
| `security.basic` | HTTP Basic configuration | `{}` |
|
||||
@@ -1226,6 +1393,17 @@ security:
|
||||
|
||||
Confused? Read [Securing Gatus with OIDC using Auth0](https://twin.sh/articles/56/securing-gatus-with-oidc-using-auth0).
|
||||
|
||||
### TLS Encryption
|
||||
Gatus supports basic encryption with TLS. To enable this, certificate files in PEM format have to be provided.
|
||||
|
||||
The example below shows an example configuration which makes gatus respond on port 4443 to HTTPS requests:
|
||||
```yaml
|
||||
web:
|
||||
port: 4443
|
||||
tls:
|
||||
certificate-file: "certificate.crt"
|
||||
private-key-file: "private.key"
|
||||
```
|
||||
|
||||
### Metrics
|
||||
To enable metrics, you must set `metrics` to `true`. Doing so will expose Prometheus-friendly metrics at the `/metrics`
|
||||
@@ -1242,23 +1420,45 @@ endpoint on the same port your application is configured to run on (`web.port`).
|
||||
See [examples/docker-compose-grafana-prometheus](.examples/docker-compose-grafana-prometheus) for further documentation as well as an example.
|
||||
|
||||
|
||||
### Connectivity
|
||||
| Parameter | Description | Default |
|
||||
|:--------------------------------|:-------------------------------------------|:--------------|
|
||||
| `connectivity` | Connectivity configuration | `{}` |
|
||||
| `connectivity.checker` | Connectivity checker configuration | Required `{}` |
|
||||
| `connectivity.checker.target` | Host to use for validating connectivity | Required `""` |
|
||||
| `connectivity.checker.interval` | Interval at which to validate connectivity | `1m` |
|
||||
|
||||
While Gatus is used to monitor other services, it is possible for Gatus itself to lose connectivity to the internet.
|
||||
In order to prevent Gatus from reporting endpoints as unhealthy when Gatus itself is unhealthy, you may configure
|
||||
Gatus to periodically check for internet connectivity.
|
||||
|
||||
All endpoint executions are skipped while the connectivity checker deems connectivity to be down.
|
||||
|
||||
```yaml
|
||||
connectivity:
|
||||
checker:
|
||||
target: 1.1.1.1:53
|
||||
interval: 60s
|
||||
```
|
||||
|
||||
|
||||
### Remote instances (EXPERIMENTAL)
|
||||
This feature allows you to retrieve endpoint statuses from a remote Gatus instance.
|
||||
|
||||
There are two main use cases for this:
|
||||
- You have multiple Gatus instances running on different machines, and you wish to visually expose the statuses through a single dashboard
|
||||
- You have one or more Gatus instances that are not publicly accessible (e.g. behind a firewall), and you wish to retrieve
|
||||
- You have one or more Gatus instances that are not publicly accessible (e.g. behind a firewall), and you wish to retrieve
|
||||
|
||||
This is an experimental feature. It may be removed or updated in a breaking manner at any time. Furthermore,
|
||||
there are known issues with this feature. If you'd like to provide some feedback, please write a comment in [#64](https://github.com/TwiN/gatus/issues/64).
|
||||
Use at your own risk.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:---------------------------------------------|:---------------|
|
||||
| `remote` | Remote configuration | `{}` |
|
||||
| `remote.instances` | List of remote instances | Required `[]` |
|
||||
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||
| Parameter | Description | Default |
|
||||
|:-----------------------------------|:---------------------------------------------|:--------------|
|
||||
| `remote` | Remote configuration | `{}` |
|
||||
| `remote.instances` | List of remote instances | Required `[]` |
|
||||
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||
|
||||
```yaml
|
||||
remote:
|
||||
@@ -1316,7 +1516,7 @@ Gatus can be deployed on Terraform by using the following module: [terraform-kub
|
||||
|
||||
## Running the tests
|
||||
```console
|
||||
go test ./... -mod vendor
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
|
||||
@@ -1386,11 +1586,11 @@ simple health checks used for alerting (PagerDuty/Twilio) to `30s`.
|
||||
|
||||
|
||||
### Default timeouts
|
||||
| Endpoint type | Timeout |
|
||||
|:---------------|:--------|
|
||||
| HTTP | 10s |
|
||||
| TCP | 10s |
|
||||
| ICMP | 10s |
|
||||
| Endpoint type | Timeout |
|
||||
|:--------------|:--------|
|
||||
| HTTP | 10s |
|
||||
| TCP | 10s |
|
||||
| ICMP | 10s |
|
||||
|
||||
To modify the timeout, see [Client configuration](#client-configuration).
|
||||
|
||||
@@ -1448,6 +1648,22 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
|
||||
|
||||
This works for SCTP based application.
|
||||
|
||||
### Monitoring a WebSocket endpoint
|
||||
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: example
|
||||
url: "wss://example.com/"
|
||||
body: "status"
|
||||
conditions:
|
||||
- "[CONNECTED] == true"
|
||||
- "[BODY].result >= 0"
|
||||
```
|
||||
|
||||
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
|
||||
shows whether the connection was successfully established.
|
||||
|
||||
### Monitoring an endpoint using ICMP
|
||||
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
|
||||
commonly known as "ping" or "echo":
|
||||
@@ -1485,6 +1701,28 @@ There are two placeholders that can be used in the conditions for endpoints of t
|
||||
- The placeholder `[DNS_RCODE]` resolves to the name associated to the response code returned by the query, such as
|
||||
`NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, etc.
|
||||
|
||||
### Monitoring an endpoint using SSH
|
||||
You can monitor endpoints using SSH by prefixing `endpoints[].url` with `ssh:\\`:
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: ssh-example
|
||||
url: "ssh://example.com:22" # port is optional. Default is 22.
|
||||
ssh:
|
||||
username: "username"
|
||||
password: "password"
|
||||
body: |
|
||||
{
|
||||
"command": "uptime"
|
||||
}
|
||||
interval: 1m
|
||||
conditions:
|
||||
- "[CONNECTED] == true"
|
||||
- "[STATUS] == 0"
|
||||
```
|
||||
|
||||
The following placeholders are supported for endpoints of type SSH:
|
||||
- `[CONNECTED]` resolves to `true` if the SSH connection was successful, `false` otherwise
|
||||
- `[STATUS]` resolves the exit code of the command executed on the remote server (e.g. `0` for success)
|
||||
|
||||
### Monitoring an endpoint using STARTTLS
|
||||
If you have an email server that you want to ensure there are no problems with, monitoring it through STARTTLS
|
||||
@@ -1501,7 +1739,6 @@ endpoints:
|
||||
- "[CERTIFICATE_EXPIRATION] > 48h"
|
||||
```
|
||||
|
||||
|
||||
### Monitoring an endpoint using TLS
|
||||
Monitoring endpoints using SSL/TLS encryption, such as LDAP over TLS, can help detect certificate expiration:
|
||||
```yaml
|
||||
@@ -1531,7 +1768,7 @@ endpoints:
|
||||
```
|
||||
|
||||
> ⚠ The usage of the `[DOMAIN_EXPIRATION]` placeholder requires Gatus to send a request to the official IANA WHOIS service [through a library](https://github.com/TwiN/whois)
|
||||
and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
|
||||
and in some cases, a secondary request to a TLD-specific WHOIS server (e.g. `whois.nic.sh`).
|
||||
To prevent the WHOIS service from throttling your IP address if you send too many requests, Gatus will prevent you from
|
||||
using the `[DOMAIN_EXPIRATION]` placeholder on an endpoint with an interval of less than `5m`.
|
||||
|
||||
@@ -1619,6 +1856,12 @@ The configuration above will result in a dashboard that looks like this:
|
||||

|
||||
|
||||
|
||||
### Exposing Gatus on a custom path
|
||||
Currently, you can expose the Gatus UI using a fully qualified domain name (FQDN) such as `status.example.org`. However, it does not support path-based routing, which means you cannot expose it through a URL like `example.org/status/`.
|
||||
|
||||
For more information, see https://github.com/TwiN/gatus/issues/88.
|
||||
|
||||
|
||||
### Exposing Gatus on a custom port
|
||||
By default, Gatus is exposed on port `8080`, but you may specify a different port by setting the `web.port` parameter:
|
||||
```yaml
|
||||
@@ -1634,6 +1877,10 @@ web:
|
||||
```
|
||||
|
||||
|
||||
### Configuring a startup delay
|
||||
If, for any reason, you need Gatus to wait for a given amount of time before monitoring the endpoints on application start, you can use the `GATUS_DELAY_START_SECONDS` environment variable to make Gatus sleep on startup.
|
||||
|
||||
|
||||
### Keeping your configuration small
|
||||
While not specific to Gatus, you can leverage YAML anchors to create a default configuration.
|
||||
If you have a large configuration file, this should help you keep things clean.
|
||||
@@ -1657,7 +1904,7 @@ endpoints:
|
||||
url: "https://example.org"
|
||||
|
||||
- name: anchor-example-2
|
||||
<<: *defaults
|
||||
<<: *defaults
|
||||
group: example # This will override the group defined in &defaults
|
||||
url: "https://example.com"
|
||||
|
||||
@@ -1670,6 +1917,30 @@ endpoints:
|
||||
```
|
||||
</details>
|
||||
|
||||
### Proxy client configuration
|
||||
|
||||
You can configure a proxy for the client to use by setting the `proxy-url` parameter in the client configuration.
|
||||
|
||||
```yaml
|
||||
endpoints:
|
||||
- name: website
|
||||
url: "https://twin.sh/health"
|
||||
client:
|
||||
proxy-url: http://proxy.example.com:8080
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
```
|
||||
|
||||
### How to fix 431 Request Header Fields Too Large error
|
||||
Depending on where your environment is deployed and what kind of middleware or reverse proxy sits in front of Gatus,
|
||||
you may run into this issue. This could be because the request headers are too large, e.g. big cookies.
|
||||
|
||||
By default, `web.read-buffer-size` is set to `8192`, but increasing this value like so will increase the read buffer size:
|
||||
```yaml
|
||||
web:
|
||||
read-buffer-size: 32768
|
||||
```
|
||||
|
||||
|
||||
### Badges
|
||||
#### Uptime
|
||||
@@ -1722,6 +1993,25 @@ https://example.com/api/v1/endpoints/core_frontend/health/badge.svg
|
||||
```
|
||||
|
||||
|
||||
#### Health (Shields.io)
|
||||

|
||||
|
||||
The path to generate a badge is the following:
|
||||
```
|
||||
/api/v1/endpoints/{key}/health/badge.shields
|
||||
```
|
||||
Where:
|
||||
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||
|
||||
For instance, if you want the current status of the endpoint `frontend` in the group `core`,
|
||||
the URL would look like this:
|
||||
```
|
||||
https://example.com/api/v1/endpoints/core_frontend/health/badge.shields
|
||||
```
|
||||
|
||||
See more information about the Shields.io badge endpoint [here](https://shields.io/badges/endpoint-badge).
|
||||
|
||||
|
||||
#### Response time
|
||||

|
||||

|
||||
@@ -1736,10 +2026,10 @@ Where:
|
||||
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`.
|
||||
|
||||
|
||||
##### How to change the color thresholds of the response time badge
|
||||
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
|
||||
The values in the array correspond to the levels [Awesome, Great, Good, Passable, Bad]
|
||||
All five values must be given in milliseconds (ms).
|
||||
##### How to change the color thresholds of the response time badge
|
||||
To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
|
||||
The values in the array correspond to the levels [Awesome, Great, Good, Passable, Bad]
|
||||
All five values must be given in milliseconds (ms).
|
||||
|
||||
```
|
||||
endpoints:
|
||||
@@ -1777,13 +2067,11 @@ The API will return a JSON payload with the `Content-Type` response header set t
|
||||
No such header is required to query the API.
|
||||
|
||||
|
||||
### Installing as binary
|
||||
You can download Gatus as a binary using the following command:
|
||||
```
|
||||
go install github.com/TwiN/gatus/v5@latest
|
||||
```
|
||||
|
||||
### High level design overview
|
||||

|
||||
|
||||
|
||||
## Sponsors
|
||||
You can find the full list of sponsors [here](https://github.com/sponsors/TwiN).
|
||||
|
||||
_There is currently no sponsors_
|
||||
|
||||
<!-- [<img src="https://github.com/$user.png" width="50" />](https://github.com/$user) -->
|
||||
|
||||
@@ -5,6 +5,9 @@ package alert
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// TypeAWSSES is the Type for the awsses alerting provider
|
||||
TypeAWSSES Type = "aws-ses"
|
||||
|
||||
// TypeCustom is the Type for the custom alerting provider
|
||||
TypeCustom Type = "custom"
|
||||
|
||||
@@ -17,9 +20,15 @@ const (
|
||||
// TypeGitHub is the Type for the github alerting provider
|
||||
TypeGitHub Type = "github"
|
||||
|
||||
// TypeGitLab is the Type for the gitlab alerting provider
|
||||
TypeGitLab Type = "gitlab"
|
||||
|
||||
// TypeGoogleChat is the Type for the googlechat alerting provider
|
||||
TypeGoogleChat Type = "googlechat"
|
||||
|
||||
// TypeGotify is the Type for the gotify alerting provider
|
||||
TypeGotify Type = "gotify"
|
||||
|
||||
// TypeMatrix is the Type for the matrix alerting provider
|
||||
TypeMatrix Type = "matrix"
|
||||
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/gotify"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/messagebird"
|
||||
@@ -27,6 +30,9 @@ import (
|
||||
|
||||
// Config is the configuration for alerting providers
|
||||
type Config struct {
|
||||
// AWSSimpleEmailService is the configuration for the aws-ses alerting provider
|
||||
AWSSimpleEmailService *awsses.AlertProvider `yaml:"aws-ses,omitempty"`
|
||||
|
||||
// Custom is the configuration for the custom alerting provider
|
||||
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
|
||||
|
||||
@@ -39,9 +45,15 @@ type Config struct {
|
||||
// GitHub is the configuration for the github alerting provider
|
||||
GitHub *github.AlertProvider `yaml:"github,omitempty"`
|
||||
|
||||
// GitLab is the configuration for the gitlab alerting provider
|
||||
GitLab *gitlab.AlertProvider `yaml:"gitlab,omitempty"`
|
||||
|
||||
// GoogleChat is the configuration for the googlechat alerting provider
|
||||
GoogleChat *googlechat.AlertProvider `yaml:"googlechat,omitempty"`
|
||||
|
||||
// Gotify is the configuration for the gotify alerting provider
|
||||
Gotify *gotify.AlertProvider `yaml:"gotify,omitempty"`
|
||||
|
||||
// Matrix is the configuration for the matrix alerting provider
|
||||
Matrix *matrix.AlertProvider `yaml:"matrix,omitempty"`
|
||||
|
||||
@@ -81,7 +93,8 @@ func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provi
|
||||
entityType := reflect.TypeOf(config).Elem()
|
||||
for i := 0; i < entityType.NumField(); i++ {
|
||||
field := entityType.Field(i)
|
||||
if strings.ToLower(field.Name) == string(alertType) {
|
||||
tag := strings.Split(field.Tag.Get("yaml"), ",")[0]
|
||||
if tag == string(alertType) {
|
||||
fieldValue := reflect.ValueOf(config).Elem().Field(i)
|
||||
if fieldValue.IsNil() {
|
||||
return nil
|
||||
|
||||
166
alerting/provider/awsses/awsses.go
Normal file
166
alerting/provider/awsses/awsses.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package awsses
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ses"
|
||||
)
|
||||
|
||||
const (
|
||||
CharSet = "UTF-8"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
|
||||
type AlertProvider struct {
|
||||
AccessKeyID string `yaml:"access-key-id"`
|
||||
SecretAccessKey string `yaml:"secret-access-key"`
|
||||
Region string `yaml:"region"`
|
||||
|
||||
From string `yaml:"from"`
|
||||
To string `yaml:"to"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||
Overrides []Override `yaml:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// Override is a case under which the default integration is overridden
|
||||
type Override struct {
|
||||
Group string `yaml:"group"`
|
||||
To string `yaml:"to"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
registeredGroups := make(map[string]bool)
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 {
|
||||
return false
|
||||
}
|
||||
registeredGroups[override.Group] = true
|
||||
}
|
||||
}
|
||||
|
||||
// if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate,
|
||||
// otherwise if neither are specified, then we'll fall back on IAM authentication.
|
||||
return len(provider.From) > 0 && len(provider.To) > 0 &&
|
||||
((len(provider.AccessKeyID) == 0 && len(provider.SecretAccessKey) == 0) || (len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0))
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
sess, err := provider.CreateSesSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svc := ses.New(sess)
|
||||
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
||||
|
||||
input := &ses.SendEmailInput{
|
||||
Destination: &ses.Destination{
|
||||
ToAddresses: aws.StringSlice(emails),
|
||||
},
|
||||
Message: &ses.Message{
|
||||
Body: &ses.Body{
|
||||
Text: &ses.Content{
|
||||
Charset: aws.String(CharSet),
|
||||
Data: aws.String(body),
|
||||
},
|
||||
},
|
||||
Subject: &ses.Content{
|
||||
Charset: aws.String(CharSet),
|
||||
Data: aws.String(subject),
|
||||
},
|
||||
},
|
||||
Source: aws.String(provider.From),
|
||||
}
|
||||
_, err = svc.SendEmail(input)
|
||||
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
switch aerr.Code() {
|
||||
case ses.ErrCodeMessageRejected:
|
||||
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
|
||||
case ses.ErrCodeMailFromDomainNotVerifiedException:
|
||||
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
|
||||
case ses.ErrCodeConfigurationSetDoesNotExistException:
|
||||
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
|
||||
default:
|
||||
fmt.Println(aerr.Error())
|
||||
}
|
||||
} else {
|
||||
// Print the error, cast err to awserr.Error to get the Code and
|
||||
// Message from an error.
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMessageSubjectAndBody builds the message subject and body
|
||||
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
||||
var subject, message, results string
|
||||
if resolved {
|
||||
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = "✅"
|
||||
} else {
|
||||
prefix = "❌"
|
||||
}
|
||||
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = "\n\nAlert description: " + alertDescription
|
||||
}
|
||||
return subject, message + description + "\n\nCondition results:\n" + results
|
||||
}
|
||||
|
||||
// getToForGroup returns the appropriate email integration to for a given group
|
||||
func (provider *AlertProvider) getToForGroup(group string) string {
|
||||
if provider.Overrides != nil {
|
||||
for _, override := range provider.Overrides {
|
||||
if group == override.Group {
|
||||
return override.To
|
||||
}
|
||||
}
|
||||
}
|
||||
return provider.To
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
func (provider AlertProvider) CreateSesSession() (*session.Session, error) {
|
||||
config := &aws.Config{
|
||||
Region: aws.String(provider.Region),
|
||||
}
|
||||
|
||||
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
|
||||
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
|
||||
}
|
||||
|
||||
return session.NewSession(config)
|
||||
}
|
||||
188
alerting/provider/awsses/awsses_test.go
Normal file
188
alerting/provider/awsses/awsses_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package awsses
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
)
|
||||
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
invalidProvider := AlertProvider{}
|
||||
if invalidProvider.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
}
|
||||
invalidProviderWithOneKey := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1"}
|
||||
if invalidProviderWithOneKey.IsValid() {
|
||||
t.Error("provider shouldn't have been valid")
|
||||
}
|
||||
validProvider := AlertProvider{From: "from@example.com", To: "to@example.com"}
|
||||
if !validProvider.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
validProviderWithKeys := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1", SecretAccessKey: "1"}
|
||||
if !validProviderWithKeys.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "to@example.com",
|
||||
Group: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideGroup.IsValid() {
|
||||
t.Error("provider Group shouldn't have been valid")
|
||||
}
|
||||
providerWithInvalidOverrideTo := AlertProvider{
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if providerWithInvalidOverrideTo.IsValid() {
|
||||
t.Error("provider integration key shouldn't have been valid")
|
||||
}
|
||||
providerWithValidOverride := AlertProvider{
|
||||
From: "from@example.com",
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
To: "to@example.com",
|
||||
Group: "group",
|
||||
},
|
||||
},
|
||||
}
|
||||
if !providerWithValidOverride.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedSubject string
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedSubject: "[endpoint-name] Alert triggered",
|
||||
ExpectedBody: "An alert for endpoint-name has been triggered due to having failed 3 time(s) in a row\n\nAlert description: description-1\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedSubject: "[endpoint-name] Alert resolved",
|
||||
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||
&core.Endpoint{Name: "endpoint-name"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if subject != scenario.ExpectedSubject {
|
||||
t.Errorf("expected subject to be %s, got %s", scenario.ExpectedSubject, subject)
|
||||
}
|
||||
if body != scenario.ExpectedBody {
|
||||
t.Errorf("expected body to be %s, got %s", scenario.ExpectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_getToForGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
InputGroup string
|
||||
ExpectedOutput string
|
||||
}{
|
||||
{
|
||||
Name: "provider-no-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-no-override-specify-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: nil,
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-no-group-should-default",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
To: "to01@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "",
|
||||
ExpectedOutput: "to@example.com",
|
||||
},
|
||||
{
|
||||
Name: "provider-with-override-specify-group-should-override",
|
||||
Provider: AlertProvider{
|
||||
To: "to@example.com",
|
||||
Overrides: []Override{
|
||||
{
|
||||
Group: "group",
|
||||
To: "to01@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
InputGroup: "group",
|
||||
ExpectedOutput: "to01@example.com",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
if got := tt.Provider.getToForGroup(tt.InputGroup); got != tt.ExpectedOutput {
|
||||
t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,6 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -217,10 +217,10 @@ func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ type AlertProvider struct {
|
||||
|
||||
// Overrides is a list of Override that may be prioritized over the default configuration
|
||||
Overrides []Override `yaml:"overrides,omitempty"`
|
||||
|
||||
// Title is the title of the message that will be sent
|
||||
Title string `yaml:"title,omitempty"`
|
||||
}
|
||||
|
||||
// Override is a case under which the default integration is overridden
|
||||
@@ -105,11 +108,15 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\n> " + alertDescription
|
||||
}
|
||||
title := ":helmet_with_white_cross: Gatus"
|
||||
if provider.Title != "" {
|
||||
title = provider.Title
|
||||
}
|
||||
body, _ := json.Marshal(Body{
|
||||
Content: "",
|
||||
Embeds: []Embed{
|
||||
{
|
||||
Title: ":helmet_with_white_cross: Gatus",
|
||||
Title: title,
|
||||
Description: message + description,
|
||||
Color: colorCode,
|
||||
Fields: []Field{
|
||||
@@ -138,6 +145,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
defer client.InjectHTTPClient(nil)
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
title := "provider-title"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
@@ -111,6 +112,16 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
}),
|
||||
ExpectedError: true,
|
||||
},
|
||||
{
|
||||
Name: "triggered-with-modified-title",
|
||||
Provider: AlertProvider{Title: title},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||
}),
|
||||
ExpectedError: false,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
@@ -139,6 +150,7 @@ func TestAlertProvider_Send(t *testing.T) {
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
title := "provider-title"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
@@ -160,6 +172,13 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"color\":3066993,\"fields\":[{\"name\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n:white_check_mark: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
|
||||
},
|
||||
{
|
||||
Name: "triggered-with-modified-title",
|
||||
Provider: AlertProvider{Title: title},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
@@ -187,10 +206,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
@@ -19,6 +21,9 @@ type AlertProvider struct {
|
||||
Port int `yaml:"port"`
|
||||
To string `yaml:"to"`
|
||||
|
||||
// ClientConfig is the configuration of the client used to communicate with the provider's target
|
||||
ClientConfig *client.Config `yaml:"client,omitempty"`
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
@@ -44,7 +49,7 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
return len(provider.From) > 0 && len(provider.Password) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
|
||||
return len(provider.From) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
@@ -61,7 +66,23 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
m.SetHeader("To", strings.Split(provider.getToForGroup(endpoint.Group), ",")...)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/plain", body)
|
||||
d := gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
|
||||
var d *gomail.Dialer
|
||||
if len(provider.Password) == 0 {
|
||||
// Get the domain in the From address
|
||||
localName := "localhost"
|
||||
fromParts := strings.Split(provider.From, `@`)
|
||||
if len(fromParts) == 2 {
|
||||
localName = fromParts[1]
|
||||
}
|
||||
// Create a dialer with no authentication
|
||||
d = &gomail.Dialer{Host: provider.Host, Port: provider.Port, LocalName: localName}
|
||||
} else {
|
||||
// Create an authenticated dialer
|
||||
d = gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
|
||||
}
|
||||
if provider.ClientConfig != nil && provider.ClientConfig.Insecure {
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
return d.DialAndSend(m)
|
||||
}
|
||||
|
||||
@@ -104,6 +125,6 @@ func (provider *AlertProvider) getToForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -18,6 +18,13 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithNoCredentials(t *testing.T) {
|
||||
validProvider := AlertProvider{From: "from@example.com", Host: "smtp-relay.gmail.com", Port: 587, To: "to@example.com"}
|
||||
if !validProvider.IsValid() {
|
||||
t.Error("provider should've been valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||
providerWithInvalidOverrideGroup := AlertProvider{
|
||||
Overrides: []Override{
|
||||
@@ -111,10 +118,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,6 @@ func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *al
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -149,10 +149,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
147
alerting/provider/gitlab/gitlab.go
Normal file
147
alerting/provider/gitlab/gitlab.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using GitLab
|
||||
type AlertProvider struct {
|
||||
WebhookURL string `yaml:"webhook-url"` // The webhook url provided by GitLab
|
||||
AuthorizationKey string `yaml:"authorization-key"` // The authorization key provided by GitLab
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical
|
||||
Severity string `yaml:"severity,omitempty"`
|
||||
// MonitoringTool overrides the name sent to gitlab. Defaults to gatus
|
||||
MonitoringTool string `yaml:"monitoring-tool,omitempty"`
|
||||
// EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard.
|
||||
EnvironmentName string `yaml:"environment-name,omitempty"`
|
||||
// Service affected. Defaults to endpoint display name
|
||||
Service string `yaml:"service,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
if len(provider.AuthorizationKey) == 0 || len(provider.WebhookURL) == 0 {
|
||||
return false
|
||||
}
|
||||
// Validate format of the repository URL
|
||||
_, err := url.Parse(provider.WebhookURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
|
||||
// or closes the relevant issue(s) if the resolved parameter passed is true.
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
if len(alert.ResolveKey) == 0 {
|
||||
alert.ResolveKey = uuid.NewString()
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", provider.AuthorizationKey))
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode > 399 {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("call to provider alert returned status code %d: %s", response.StatusCode, string(body))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type AlertBody struct {
|
||||
Title string `json:"title,omitempty"` // The title of the alert.
|
||||
Description string `json:"description,omitempty"` // A high-level summary of the problem.
|
||||
StartTime string `json:"start_time,omitempty"` // The time of the alert. If none is provided, a current time is used.
|
||||
EndTime string `json:"end_time,omitempty"` // The resolution time of the alert. If provided, the alert is resolved.
|
||||
Service string `json:"service,omitempty"` // The affected service.
|
||||
MonitoringTool string `json:"monitoring_tool,omitempty"` // The name of the associated monitoring tool.
|
||||
Hosts string `json:"hosts,omitempty"` // One or more hosts, as to where this incident occurred.
|
||||
Severity string `json:"severity,omitempty"` // The severity of the alert. Case-insensitive. Can be one of: critical, high, medium, low, info, unknown. Defaults to critical if missing or value is not in this list.
|
||||
Fingerprint string `json:"fingerprint,omitempty"` // The unique identifier of the alert. This can be used to group occurrences of the same alert.
|
||||
GitlabEnvironmentName string `json:"gitlab_environment_name,omitempty"` // The name of the associated GitLab environment. Required to display alerts on a dashboard.
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) monitoringTool() string {
|
||||
if len(provider.MonitoringTool) > 0 {
|
||||
return provider.MonitoringTool
|
||||
}
|
||||
return "gatus"
|
||||
}
|
||||
|
||||
func (provider *AlertProvider) service(endpoint *core.Endpoint) string {
|
||||
if len(provider.Service) > 0 {
|
||||
return provider.Service
|
||||
}
|
||||
return endpoint.DisplayName()
|
||||
}
|
||||
|
||||
// buildAlertBody builds the body of the alert
|
||||
func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
body := AlertBody{
|
||||
Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(endpoint)),
|
||||
StartTime: result.Timestamp.Format(time.RFC3339),
|
||||
Service: provider.service(endpoint),
|
||||
MonitoringTool: provider.monitoringTool(),
|
||||
Hosts: endpoint.URL,
|
||||
GitlabEnvironmentName: provider.EnvironmentName,
|
||||
Severity: provider.Severity,
|
||||
Fingerprint: alert.ResolveKey,
|
||||
}
|
||||
if resolved {
|
||||
body.EndTime = result.Timestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
var results string
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = ":white_check_mark:"
|
||||
} else {
|
||||
prefix = ":x:"
|
||||
}
|
||||
results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
|
||||
}
|
||||
var description string
|
||||
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||
description = ":\n> " + alertDescription
|
||||
}
|
||||
var message string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
body.Description = message + description + "\n\n## Condition results\n" + results
|
||||
|
||||
json, _ := json.Marshal(body)
|
||||
return json
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
158
alerting/provider/gitlab/gitlab_test.go
Normal file
158
alerting/provider/gitlab/gitlab_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/test"
|
||||
)
|
||||
|
||||
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Expected bool
|
||||
}{
|
||||
{
|
||||
Name: "invalid",
|
||||
Provider: AlertProvider{WebhookURL: "", AuthorizationKey: ""},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "missing-webhook-url",
|
||||
Provider: AlertProvider{WebhookURL: "", AuthorizationKey: "12345"},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "missing-authorization-key",
|
||||
Provider: AlertProvider{WebhookURL: "https://gitlab.com/hlidotbe/text/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json", AuthorizationKey: ""},
|
||||
Expected: false,
|
||||
},
|
||||
{
|
||||
Name: "invalid-url",
|
||||
Provider: AlertProvider{WebhookURL: " http://foo.com", AuthorizationKey: "12345"},
|
||||
Expected: false,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
if scenario.Provider.IsValid() != scenario.Expected {
|
||||
t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.IsValid())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_Send(t *testing.T) {
|
||||
defer client.InjectHTTPClient(nil)
|
||||
firstDescription := "description-1"
|
||||
secondDescription := "description-2"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
MockRoundTripper test.MockRoundTripper
|
||||
ExpectedError bool
|
||||
}{
|
||||
{
|
||||
Name: "triggered-error",
|
||||
Provider: AlertProvider{WebhookURL: "https://gitlab.com/hlidotbe/text/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json", AuthorizationKey: "12345"},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedError: false,
|
||||
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "resolved-error",
|
||||
Provider: AlertProvider{WebhookURL: "https://gitlab.com/hlidotbe/text/alerts/notify/gatus/xxxxxxxxxxxxxxxx.json", AuthorizationKey: "12345"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedError: false,
|
||||
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
|
||||
err := scenario.Provider.Send(
|
||||
&core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if scenario.ExpectedError && err == nil {
|
||||
t.Error("expected error, got none")
|
||||
}
|
||||
if !scenario.ExpectedError && err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildAlertBody(t *testing.T) {
|
||||
firstDescription := "description-1"
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Endpoint core.Endpoint
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 3},
|
||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||
},
|
||||
{
|
||||
Name: "no-description",
|
||||
Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
|
||||
Provider: AlertProvider{},
|
||||
Alert: alert.Alert{FailureThreshold: 10},
|
||||
ExpectedBody: "{\"title\":\"alert(gatus): endpoint-name\",\"description\":\"An alert for *endpoint-name* has been triggered due to having failed 10 time(s) in a row\\n\\n## Condition results\\n- :white_check_mark: - `[CONNECTED] == true`\\n- :x: - `[STATUS] == 200`\\n\",\"start_time\":\"0001-01-01T00:00:00Z\",\"service\":\"endpoint-name\",\"monitoring_tool\":\"gatus\",\"hosts\":\"https://example.org\"}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildAlertBody(
|
||||
&scenario.Endpoint,
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: true},
|
||||
{Condition: "[STATUS] == 200", Success: false},
|
||||
},
|
||||
},
|
||||
false,
|
||||
)
|
||||
if strings.TrimSpace(string(body)) != strings.TrimSpace(scenario.ExpectedBody) {
|
||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -205,10 +205,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
105
alerting/provider/gotify/gotify.go
Normal file
105
alerting/provider/gotify/gotify.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package gotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
)
|
||||
|
||||
const DefaultPriority = 5
|
||||
|
||||
// AlertProvider is the configuration necessary for sending an alert using Gotify
|
||||
type AlertProvider struct {
|
||||
// ServerURL is the URL of the Gotify server
|
||||
ServerURL string `yaml:"server-url"`
|
||||
|
||||
// Token is the token to use when sending a message to the Gotify server
|
||||
Token string `yaml:"token"`
|
||||
|
||||
// Priority is the priority of the message
|
||||
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
|
||||
// Title is the title of the message that will be sent
|
||||
Title string `yaml:"title,omitempty"`
|
||||
}
|
||||
|
||||
// IsValid returns whether the provider's configuration is valid
|
||||
func (provider *AlertProvider) IsValid() bool {
|
||||
if provider.Priority == 0 {
|
||||
provider.Priority = DefaultPriority
|
||||
}
|
||||
return len(provider.ServerURL) > 0 && len(provider.Token) > 0
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||
buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
|
||||
request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode > 399 {
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
return fmt.Errorf("failed to send alert to Gotify: %s", string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Body struct {
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = "✓"
|
||||
} else {
|
||||
prefix = "✕"
|
||||
}
|
||||
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
|
||||
}
|
||||
if len(alert.GetDescription()) > 0 {
|
||||
message += " with the following description: " + alert.GetDescription()
|
||||
}
|
||||
message += results
|
||||
title := "Gatus: " + endpoint.DisplayName()
|
||||
if provider.Title != "" {
|
||||
title = provider.Title
|
||||
}
|
||||
body, _ := json.Marshal(Body{
|
||||
Message: message,
|
||||
Title: title,
|
||||
Priority: provider.Priority,
|
||||
})
|
||||
return body
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
105
alerting/provider/gotify/gotify_test.go
Normal file
105
alerting/provider/gotify/gotify_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package gotify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
)
|
||||
|
||||
func TestAlertProvider_IsValid(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
provider AlertProvider
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-server-url",
|
||||
provider: AlertProvider{ServerURL: "", Token: "faketoken"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid-app-token",
|
||||
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: ""},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no-priority-should-use-default-value",
|
||||
provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
if scenario.provider.IsValid() != scenario.expected {
|
||||
t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
var (
|
||||
description = "custom-description"
|
||||
//title = "custom-title"
|
||||
endpoint = "custom-endpoint"
|
||||
)
|
||||
scenarios := []struct {
|
||||
Name string
|
||||
Provider AlertProvider
|
||||
Alert alert.Alert
|
||||
Resolved bool
|
||||
ExpectedBody string
|
||||
}{
|
||||
{
|
||||
Name: "triggered",
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been resolved after passing successfully 5 time(s) in a row with the following description: %s\\n✓ - [CONNECTED] == true\\n✓ - [STATUS] == 200\",\"title\":\"Gatus: custom-endpoint\",\"priority\":0}", endpoint, description),
|
||||
},
|
||||
{
|
||||
Name: "custom-title",
|
||||
Provider: AlertProvider{ServerURL: "https://gotify.example.com", Token: "faketoken", Title: "custom-title"},
|
||||
Alert: alert.Alert{Description: &description, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: fmt.Sprintf("{\"message\":\"An alert for `%s` has been triggered due to having failed 3 time(s) in a row with the following description: %s\\n✕ - [CONNECTED] == true\\n✕ - [STATUS] == 200\",\"title\":\"custom-title\",\"priority\":0}", endpoint, description),
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
body := scenario.Provider.buildRequestBody(
|
||||
&core.Endpoint{Name: endpoint},
|
||||
&scenario.Alert,
|
||||
&core.Result{
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||
},
|
||||
},
|
||||
scenario.Resolved,
|
||||
)
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)
|
||||
}
|
||||
out := make(map[string]interface{})
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
t.Error("expected body to be valid JSON, got error:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,6 @@ func randStringBytes(n int) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -219,10 +219,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -190,10 +190,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
@@ -23,6 +24,7 @@ type AlertProvider struct {
|
||||
Topic string `yaml:"topic"`
|
||||
URL string `yaml:"url,omitempty"` // Defaults to DefaultURL
|
||||
Priority int `yaml:"priority,omitempty"` // Defaults to DefaultPriority
|
||||
Token string `yaml:"token,omitempty"` // Defaults to ""
|
||||
|
||||
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
|
||||
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
|
||||
@@ -36,7 +38,11 @@ func (provider *AlertProvider) IsValid() bool {
|
||||
if provider.Priority == 0 {
|
||||
provider.Priority = DefaultPriority
|
||||
}
|
||||
return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6
|
||||
isTokenValid := true
|
||||
if len(provider.Token) > 0 {
|
||||
isTokenValid = strings.HasPrefix(provider.Token, "tk_")
|
||||
}
|
||||
return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6 && isTokenValid
|
||||
}
|
||||
|
||||
// Send an alert using the provider
|
||||
@@ -47,6 +53,9 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
if len(provider.Token) > 0 {
|
||||
request.Header.Set("Authorization", "Bearer "+provider.Token)
|
||||
}
|
||||
response, err := client.GetHTTPClient(nil).Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -69,17 +78,27 @@ type Body struct {
|
||||
|
||||
// buildRequestBody builds the request body for the provider
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, tag string
|
||||
var message, results, tag string
|
||||
if resolved {
|
||||
tag = "white_check_mark"
|
||||
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row"
|
||||
} else {
|
||||
tag = "x"
|
||||
tag = "rotating_light"
|
||||
message = "An alert has been triggered due to having failed " + strconv.Itoa(alert.FailureThreshold) + " time(s) in a row"
|
||||
}
|
||||
for _, conditionResult := range result.ConditionResults {
|
||||
var prefix string
|
||||
if conditionResult.Success {
|
||||
prefix = "🟢"
|
||||
} else {
|
||||
prefix = "🔴"
|
||||
}
|
||||
results += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition)
|
||||
}
|
||||
if len(alert.GetDescription()) > 0 {
|
||||
message += " with the following description: " + alert.GetDescription()
|
||||
}
|
||||
message += results
|
||||
body, _ := json.Marshal(Body{
|
||||
Topic: provider.Topic,
|
||||
Title: "Gatus: " + endpoint.DisplayName(),
|
||||
@@ -91,6 +110,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||
provider: AlertProvider{Topic: "example", Priority: 1},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "valid-with-token",
|
||||
provider: AlertProvider{Topic: "example", Priority: 1, Token: "tk_faketoken"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-token",
|
||||
provider: AlertProvider{Topic: "example", Priority: 1, Token: "xx_faketoken"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid-topic",
|
||||
provider: AlertProvider{URL: "https://ntfy.sh", Topic: "", Priority: 1},
|
||||
@@ -69,14 +79,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 1},
|
||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: false,
|
||||
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1","tags":["x"],"priority":1}`,
|
||||
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been triggered due to having failed 3 time(s) in a row with the following description: description-1\n🔴 [CONNECTED] == true\n🔴 [STATUS] == 200","tags":["rotating_light"],"priority":1}`,
|
||||
},
|
||||
{
|
||||
Name: "resolved",
|
||||
Provider: AlertProvider{URL: "https://ntfy.sh", Topic: "example", Priority: 2},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2","tags":["white_check_mark"],"priority":2}`,
|
||||
ExpectedBody: `{"topic":"example","title":"Gatus: endpoint-name","message":"An alert has been resolved after passing successfully 5 time(s) in a row with the following description: description-2\n🟢 [CONNECTED] == true\n🟢 [STATUS] == 200","tags":["white_check_mark"],"priority":2}`,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
@@ -206,7 +206,7 @@ func (provider *AlertProvider) priority() string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ func (provider *AlertProvider) getIntegrationKeyForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
|
||||
@@ -237,10 +237,10 @@ func TestAlertProvider_getIntegrationKeyForGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package provider
|
||||
|
||||
import (
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/github"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/gitlab"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/googlechat"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/matrix"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider/mattermost"
|
||||
@@ -57,10 +59,12 @@ func ParseWithDefaultAlert(providerDefaultAlert, endpointAlert *alert.Alert) {
|
||||
|
||||
var (
|
||||
// Validate interface implementation on compile
|
||||
_ AlertProvider = (*awsses.AlertProvider)(nil)
|
||||
_ AlertProvider = (*custom.AlertProvider)(nil)
|
||||
_ AlertProvider = (*discord.AlertProvider)(nil)
|
||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||
_ AlertProvider = (*github.AlertProvider)(nil)
|
||||
_ AlertProvider = (*gitlab.AlertProvider)(nil)
|
||||
_ AlertProvider = (*googlechat.AlertProvider)(nil)
|
||||
_ AlertProvider = (*matrix.AlertProvider)(nil)
|
||||
_ AlertProvider = (*mattermost.AlertProvider)(nil)
|
||||
|
||||
@@ -107,6 +107,6 @@ func (provider *AlertProvider) priority() int {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -172,10 +172,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -205,10 +205,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,6 @@ func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -186,10 +186,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ type Body struct {
|
||||
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte {
|
||||
var message, results string
|
||||
if resolved {
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||
} else {
|
||||
message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold)
|
||||
}
|
||||
@@ -97,6 +97,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
Provider: AlertProvider{ID: "123"},
|
||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||
Resolved: true,
|
||||
ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 3 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
|
||||
ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 5 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}",
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
@@ -159,10 +159,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
|
||||
}
|
||||
|
||||
// GetDefaultAlert returns the provider's default alert configuration
|
||||
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
|
||||
return provider.DefaultAlert
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
if (&AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||
t.Error("expected default alert to be not nil")
|
||||
}
|
||||
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
if (&AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||
t.Error("expected default alert to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
120
api/api.go
Normal file
120
api/api.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/web"
|
||||
static "github.com/TwiN/gatus/v5/web"
|
||||
"github.com/TwiN/health"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
fiberfs "github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/fiber/v2/middleware/redirect"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
router *fiber.App
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *API {
|
||||
api := &API{}
|
||||
if cfg.Web == nil {
|
||||
log.Println("[api.New] nil web config passed as parameter. This should only happen in tests. Using default web configuration")
|
||||
cfg.Web = web.GetDefaultConfig()
|
||||
}
|
||||
api.router = api.createRouter(cfg)
|
||||
return api
|
||||
}
|
||||
|
||||
func (a *API) Router() *fiber.App {
|
||||
return a.router
|
||||
}
|
||||
|
||||
func (a *API) createRouter(cfg *config.Config) *fiber.App {
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||
log.Printf("[api.ErrorHandler] %s", err.Error())
|
||||
return fiber.DefaultErrorHandler(c, err)
|
||||
},
|
||||
ReadBufferSize: cfg.Web.ReadBufferSize,
|
||||
Network: fiber.NetworkTCP,
|
||||
})
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "http://localhost:8081",
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
}
|
||||
// Middlewares
|
||||
app.Use(recover.New())
|
||||
app.Use(compress.New())
|
||||
// Define metrics handler, if necessary
|
||||
if cfg.Metrics {
|
||||
metricsHandler := promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
|
||||
DisableCompression: true,
|
||||
}))
|
||||
app.Get("/metrics", adaptor.HTTPHandler(metricsHandler))
|
||||
}
|
||||
// Define main router
|
||||
apiRouter := app.Group("/api")
|
||||
////////////////////////
|
||||
// UNPROTECTED ROUTES //
|
||||
////////////////////////
|
||||
unprotectedAPIRouter := apiRouter.Group("/")
|
||||
unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security}.GetConfig)
|
||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge)
|
||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields)
|
||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
|
||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
|
||||
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
|
||||
// SPA
|
||||
app.Get("/", SinglePageApplication(cfg.UI))
|
||||
app.Get("/endpoints/:name", SinglePageApplication(cfg.UI))
|
||||
// Health endpoint
|
||||
healthHandler := health.Handler().WithJSON(true)
|
||||
app.Get("/health", func(c *fiber.Ctx) error {
|
||||
statusCode, body := healthHandler.GetResponseStatusCodeAndBody()
|
||||
return c.Status(statusCode).Send(body)
|
||||
})
|
||||
// Everything else falls back on static content
|
||||
app.Use(redirect.New(redirect.Config{
|
||||
Rules: map[string]string{
|
||||
"/index.html": "/",
|
||||
},
|
||||
StatusCode: 301,
|
||||
}))
|
||||
staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
app.Use("/", fiberfs.New(fiberfs.Config{
|
||||
Root: http.FS(staticFileSystem),
|
||||
Index: "index.html",
|
||||
Browse: true,
|
||||
}))
|
||||
//////////////////////
|
||||
// PROTECTED ROUTES //
|
||||
//////////////////////
|
||||
// ORDER IS IMPORTANT: all routes applied AFTER the security middleware will require authn
|
||||
protectedAPIRouter := apiRouter.Group("/")
|
||||
if cfg.Security != nil {
|
||||
if err := cfg.Security.RegisterHandlers(app); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cfg.Security.ApplySecurityMiddleware(protectedAPIRouter); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
|
||||
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus)
|
||||
return app
|
||||
}
|
||||
121
api/api_test.go
Normal file
121
api/api_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
"github.com/TwiN/gatus/v5/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
ExpectedCode int
|
||||
Gzip bool
|
||||
WithSecurity bool
|
||||
}
|
||||
scenarios := []Scenario{
|
||||
{
|
||||
Name: "health",
|
||||
Path: "/health",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Path: "/metrics",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "favicon.ico",
|
||||
Path: "/favicon.ico",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "app.js",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "app.js-gzipped",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js-gzipped",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "index",
|
||||
Path: "/",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "index-html-redirect",
|
||||
Path: "/index.html",
|
||||
ExpectedCode: fiber.StatusMovedPermanently,
|
||||
},
|
||||
{
|
||||
Name: "index-should-return-200-even-if-not-authenticated",
|
||||
Path: "/",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
WithSecurity: true,
|
||||
},
|
||||
{
|
||||
Name: "endpoints-should-return-401-if-not-authenticated",
|
||||
Path: "/api/v1/endpoints/statuses",
|
||||
ExpectedCode: fiber.StatusUnauthorized,
|
||||
WithSecurity: true,
|
||||
},
|
||||
{
|
||||
Name: "config-should-return-200-even-if-not-authenticated",
|
||||
Path: "/api/v1/config",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
WithSecurity: true,
|
||||
},
|
||||
{
|
||||
Name: "config-should-always-return-200",
|
||||
Path: "/api/v1/config",
|
||||
ExpectedCode: fiber.StatusOK,
|
||||
WithSecurity: false,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
cfg := &config.Config{Metrics: true, UI: &ui.Config{}}
|
||||
if scenario.WithSecurity {
|
||||
cfg.Security = &security.Config{
|
||||
Basic: &security.BasicConfig{
|
||||
Username: "john.doe",
|
||||
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
|
||||
},
|
||||
}
|
||||
}
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,10 +35,9 @@ var (
|
||||
|
||||
// UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
//
|
||||
// Valid values for {duration}: 7d, 24h, 1h
|
||||
func UptimeBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
duration := variables["duration"]
|
||||
// Valid values for :duration -> 7d, 24h, 1h
|
||||
func UptimeBadge(c *fiber.Ctx) error {
|
||||
duration := c.Params("duration")
|
||||
var from time.Time
|
||||
switch duration {
|
||||
case "7d":
|
||||
@@ -48,35 +47,30 @@ func UptimeBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
case "1h":
|
||||
from = time.Now().Add(-2 * time.Hour) // Because uptime metrics are stored by hour, we have to cheat a little
|
||||
default:
|
||||
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
||||
return
|
||||
return c.Status(400).SendString("Durations supported: 7d, 24h, 1h")
|
||||
}
|
||||
key := variables["key"]
|
||||
key := c.Params("key")
|
||||
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
return c.Status(404).SendString(err.Error())
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return c.Status(400).SendString(err.Error())
|
||||
}
|
||||
return
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateUptimeBadgeSVG(duration, uptime))
|
||||
c.Set("Content-Type", "image/svg+xml")
|
||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Set("Expires", "0")
|
||||
return c.Status(200).Send(generateUptimeBadgeSVG(duration, uptime))
|
||||
}
|
||||
|
||||
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
//
|
||||
// Valid values for {duration}: 7d, 24h, 1h
|
||||
func ResponseTimeBadge(config *config.Config) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
duration := variables["duration"]
|
||||
// Valid values for :duration -> 7d, 24h, 1h
|
||||
func ResponseTimeBadge(config *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
duration := c.Params("duration")
|
||||
var from time.Time
|
||||
switch duration {
|
||||
case "7d":
|
||||
@@ -86,44 +80,37 @@ func ResponseTimeBadge(config *config.Config) http.HandlerFunc {
|
||||
case "1h":
|
||||
from = time.Now().Add(-2 * time.Hour) // Because response time metrics are stored by hour, we have to cheat a little
|
||||
default:
|
||||
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
||||
return
|
||||
return c.Status(400).SendString("Durations supported: 7d, 24h, 1h")
|
||||
}
|
||||
key := variables["key"]
|
||||
key := c.Params("key")
|
||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
return c.Status(404).SendString(err.Error())
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return c.Status(400).SendString(err.Error())
|
||||
}
|
||||
return
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, config))
|
||||
c.Set("Content-Type", "image/svg+xml")
|
||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Set("Expires", "0")
|
||||
return c.Status(200).Send(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, config))
|
||||
}
|
||||
}
|
||||
|
||||
// HealthBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
func HealthBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
key := variables["key"]
|
||||
func HealthBadge(c *fiber.Ctx) error {
|
||||
key := c.Params("key")
|
||||
pagingConfig := paging.NewEndpointStatusParams()
|
||||
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
return c.Status(404).SendString(err.Error())
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return c.Status(400).SendString(err.Error())
|
||||
}
|
||||
return
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
healthStatus := HealthStatusUnknown
|
||||
if len(status.Results) > 0 {
|
||||
@@ -133,11 +120,40 @@ func HealthBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
healthStatus = HealthStatusDown
|
||||
}
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateHealthBadgeSVG(healthStatus))
|
||||
c.Set("Content-Type", "image/svg+xml")
|
||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Set("Expires", "0")
|
||||
return c.Status(200).Send(generateHealthBadgeSVG(healthStatus))
|
||||
}
|
||||
|
||||
func HealthBadgeShields(c *fiber.Ctx) error {
|
||||
key := c.Params("key")
|
||||
pagingConfig := paging.NewEndpointStatusParams()
|
||||
status, err := store.Get().GetEndpointStatusByKey(key, pagingConfig.WithResults(1, 1))
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
return c.Status(404).SendString(err.Error())
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
return c.Status(400).SendString(err.Error())
|
||||
}
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
healthStatus := HealthStatusUnknown
|
||||
if len(status.Results) > 0 {
|
||||
if status.Results[0].Success {
|
||||
healthStatus = HealthStatusUp
|
||||
} else {
|
||||
healthStatus = HealthStatusDown
|
||||
}
|
||||
}
|
||||
c.Set("Content-Type", "application/json")
|
||||
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Set("Expires", "0")
|
||||
jsonData, err := generateHealthBadgeShields(healthStatus)
|
||||
if err != nil {
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
return c.Status(200).Send(jsonData)
|
||||
}
|
||||
|
||||
func generateUptimeBadgeSVG(duration string, uptime float64) []byte {
|
||||
@@ -314,6 +330,17 @@ func generateHealthBadgeSVG(healthStatus string) []byte {
|
||||
return svg
|
||||
}
|
||||
|
||||
func generateHealthBadgeShields(healthStatus string) ([]byte, error) {
|
||||
color := getBadgeShieldsColorFromHealth(healthStatus)
|
||||
data := map[string]interface{}{
|
||||
"schemaVersion": 1,
|
||||
"label": "gatus",
|
||||
"message": healthStatus,
|
||||
"color": color,
|
||||
}
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func getBadgeColorFromHealth(healthStatus string) string {
|
||||
if healthStatus == HealthStatusUp {
|
||||
return badgeColorHexAwesome
|
||||
@@ -322,3 +349,12 @@ func getBadgeColorFromHealth(healthStatus string) string {
|
||||
}
|
||||
return badgeColorHexPassable
|
||||
}
|
||||
|
||||
func getBadgeShieldsColorFromHealth(healthStatus string) string {
|
||||
if healthStatus == HealthStatusUp {
|
||||
return "brightgreen"
|
||||
} else if healthStatus == HealthStatusDown {
|
||||
return "red"
|
||||
}
|
||||
return "yellow"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -31,38 +31,13 @@ func TestBadge(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
testSuccessfulResult = core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
Errors: nil,
|
||||
Connected: true,
|
||||
Success: true,
|
||||
Timestamp: timestamp,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg)
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -135,6 +110,21 @@ func TestBadge(t *testing.T) {
|
||||
Path: "/api/v1/endpoints/invalid_key/health/badge.svg",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
Name: "badge-shields-health-up",
|
||||
Path: "/api/v1/endpoints/core_frontend/health/badge.shields",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "badge-shields-health-down",
|
||||
Path: "/api/v1/endpoints/core_backend/health/badge.shields",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "badge-shields-health-for-invalid-key",
|
||||
Path: "/api/v1/endpoints/invalid_key/health/badge.shields",
|
||||
ExpectedCode: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
Name: "chart-response-time-24h",
|
||||
Path: "/api/v1/endpoints/core_backend/response-times/24h/chart.svg",
|
||||
@@ -153,14 +143,16 @@ func TestBadge(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -266,6 +258,32 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
Endpoints: []*core.Endpoint{&firstTestEndpoint, &secondTestEndpoint},
|
||||
}
|
||||
|
||||
testSuccessfulResult := core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
Errors: nil,
|
||||
Connected: true,
|
||||
Success: true,
|
||||
Timestamp: time.Now(),
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
store.Get().Insert(&firstTestEndpoint, &testSuccessfulResult)
|
||||
store.Get().Insert(&secondTestEndpoint, &testSuccessfulResult)
|
||||
|
||||
15
api/cache.go
Normal file
15
api/cache.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gocache/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheTTL = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.FirstInFirstOut)
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/wcharczuk/go-chart/v2"
|
||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||
)
|
||||
@@ -29,9 +29,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
duration := vars["duration"]
|
||||
func ResponseTimeChart(c *fiber.Ctx) error {
|
||||
duration := c.Params("duration")
|
||||
var from time.Time
|
||||
switch duration {
|
||||
case "7d":
|
||||
@@ -39,23 +38,19 @@ func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) {
|
||||
case "24h":
|
||||
from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour)
|
||||
default:
|
||||
http.Error(writer, "Durations supported: 7d, 24h", http.StatusBadRequest)
|
||||
return
|
||||
return c.Status(400).SendString("Durations supported: 7d, 24h")
|
||||
}
|
||||
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(vars["key"], from, time.Now())
|
||||
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(c.Params("key"), from, time.Now())
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
return c.Status(404).SendString(err.Error())
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return c.Status(400).SendString(err.Error())
|
||||
}
|
||||
return
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
if len(hourlyAverageResponseTime) == 0 {
|
||||
http.Error(writer, "", http.StatusNoContent)
|
||||
return
|
||||
return c.Status(204).SendString("")
|
||||
}
|
||||
series := chart.TimeSeries{
|
||||
Name: "Average response time per hour",
|
||||
@@ -111,12 +106,13 @@ func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
Series: []chart.Series{series},
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
if err := graph.Render(chart.SVG, writer); err != nil {
|
||||
log.Println("[handler][ResponseTimeChart] Failed to render response time chart:", err.Error())
|
||||
return
|
||||
c.Set("Content-Type", "image/svg+xml")
|
||||
c.Set("Cache-Control", "no-cache, no-store")
|
||||
c.Set("Expires", "0")
|
||||
c.Status(http.StatusOK)
|
||||
if err := graph.Render(chart.SVG, c); err != nil {
|
||||
log.Println("[api][ResponseTimeChart] Failed to render response time chart:", err.Error())
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -30,7 +30,8 @@ func TestResponseTimeChart(t *testing.T) {
|
||||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg)
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -61,14 +62,16 @@ func TestResponseTimeChart(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
25
api/config.go
Normal file
25
api/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/TwiN/gatus/v5/security"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
securityConfig *security.Config
|
||||
}
|
||||
|
||||
func (handler ConfigHandler) GetConfig(c *fiber.Ctx) error {
|
||||
hasOIDC := false
|
||||
isAuthenticated := true // Default to true if no security config is set
|
||||
if handler.securityConfig != nil {
|
||||
hasOIDC = handler.securityConfig.OIDC != nil
|
||||
isAuthenticated = handler.securityConfig.IsAuthenticated(c)
|
||||
}
|
||||
// Return the config
|
||||
c.Set("Content-Type", "application/json")
|
||||
return c.Status(200).
|
||||
SendString(fmt.Sprintf(`{"oidc":%v,"authenticated":%v}`, hasOIDC, isAuthenticated))
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/security"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestConfigHandler_ServeHTTP(t *testing.T) {
|
||||
@@ -20,15 +20,27 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
|
||||
}
|
||||
handler := ConfigHandler{securityConfig: securityConfig}
|
||||
// Create a fake router. We're doing this because I need the gate to be initialized.
|
||||
securityConfig.ApplySecurityMiddleware(mux.NewRouter())
|
||||
app := fiber.New()
|
||||
app.Get("/api/v1/config", handler.GetConfig)
|
||||
err := securityConfig.ApplySecurityMiddleware(app)
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil, but was", err)
|
||||
}
|
||||
// Test the config handler
|
||||
request, _ := http.NewRequest("GET", "/api/v1/config", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusOK {
|
||||
t.Error("expected code to be 200, but was", responseRecorder.Code)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil, but was", err)
|
||||
}
|
||||
if responseRecorder.Body.String() != `{"oidc":true,"authenticated":false}` {
|
||||
t.Error("expected body to be `{\"oidc\":true,\"authenticated\":false}`, but was", responseRecorder.Body.String())
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Error("expected code to be 200, but was", response.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil, but was", err)
|
||||
}
|
||||
if string(body) != `{"oidc":true,"authenticated":false}` {
|
||||
t.Error("expected body to be `{\"oidc\":true,\"authenticated\":false}`, but was", string(body))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
@@ -15,32 +13,21 @@ import (
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"github.com/TwiN/gatus/v5/storage/store/common/paging"
|
||||
"github.com/TwiN/gocache/v2"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheTTL = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.FirstInFirstOut)
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// EndpointStatuses handles requests to retrieve all EndpointStatus
|
||||
// Due to how intensive this operation can be on the storage, this function leverages a cache.
|
||||
func EndpointStatuses(cfg *config.Config) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, r *http.Request) {
|
||||
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||
func EndpointStatuses(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||
value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
|
||||
var data []byte
|
||||
if !exists {
|
||||
var err error
|
||||
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
|
||||
if err != nil {
|
||||
log.Printf("[handler][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
log.Printf("[api][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
// ALPHA: Retrieve endpoint statuses from remote instances
|
||||
if endpointStatusesFromRemote, err := getEndpointStatusesFromRemoteInstances(cfg.Remote); err != nil {
|
||||
@@ -51,17 +38,15 @@ func EndpointStatuses(cfg *config.Config) http.HandlerFunc {
|
||||
// Marshal endpoint statuses to JSON
|
||||
data, err = json.Marshal(endpointStatuses)
|
||||
if err != nil {
|
||||
log.Printf("[handler][EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
||||
http.Error(writer, "unable to marshal object to JSON", http.StatusInternalServerError)
|
||||
return
|
||||
log.Printf("[api][EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
|
||||
return c.Status(500).SendString("unable to marshal object to JSON")
|
||||
}
|
||||
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
|
||||
} else {
|
||||
data = value.([]byte)
|
||||
}
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(data)
|
||||
c.Set("Content-Type", "application/json")
|
||||
return c.Status(200).Send(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,31 +83,25 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*cor
|
||||
}
|
||||
|
||||
// EndpointStatus retrieves a single core.EndpointStatus by group and endpoint name
|
||||
func EndpointStatus(writer http.ResponseWriter, r *http.Request) {
|
||||
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||
vars := mux.Vars(r)
|
||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(vars["key"], paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||
func EndpointStatus(c *fiber.Ctx) error {
|
||||
page, pageSize := extractPageAndPageSizeFromRequest(c)
|
||||
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
return c.Status(404).SendString(err.Error())
|
||||
}
|
||||
log.Printf("[handler][EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
log.Printf("[api][EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
|
||||
return c.Status(500).SendString(err.Error())
|
||||
}
|
||||
if endpointStatus == nil {
|
||||
log.Printf("[handler][EndpointStatus] Endpoint with key=%s not found", vars["key"])
|
||||
http.Error(writer, "not found", http.StatusNotFound)
|
||||
return
|
||||
if endpointStatus == nil { // XXX: is this check necessary?
|
||||
log.Printf("[api][EndpointStatus] Endpoint with key=%s not found", c.Params("key"))
|
||||
return c.Status(404).SendString("not found")
|
||||
}
|
||||
output, err := json.Marshal(endpointStatus)
|
||||
if err != nil {
|
||||
log.Printf("[handler][EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
|
||||
http.Error(writer, "unable to marshal object to JSON", http.StatusInternalServerError)
|
||||
return
|
||||
log.Printf("[api][EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
|
||||
return c.Status(500).SendString("unable to marshal object to JSON")
|
||||
}
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(output)
|
||||
c.Set("Content-Type", "application/json")
|
||||
return c.Status(200).Send(output)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -97,8 +98,8 @@ func TestEndpointStatus(t *testing.T) {
|
||||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg)
|
||||
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -130,14 +131,16 @@ func TestEndpointStatus(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -153,8 +156,8 @@ func TestEndpointStatuses(t *testing.T) {
|
||||
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||
firstResult.Timestamp = time.Time{}
|
||||
secondResult.Timestamp = time.Time{}
|
||||
router := CreateRouter(&config.Config{Metrics: true})
|
||||
|
||||
api := New(&config.Config{Metrics: true})
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -196,15 +199,21 @@ func TestEndpointStatuses(t *testing.T) {
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output := responseRecorder.Body.String()
|
||||
if output != scenario.ExpectedBody {
|
||||
t.Errorf("expected:\n %s\n\ngot:\n %s", scenario.ExpectedBody, output)
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil, but was", err)
|
||||
}
|
||||
if string(body) != scenario.ExpectedBody {
|
||||
t.Errorf("expected:\n %s\n\ngot:\n %s", scenario.ExpectedBody, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
30
api/spa.go
Normal file
30
api/spa.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"log"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
static "github.com/TwiN/gatus/v5/web"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func SinglePageApplication(ui *ui.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||
if err != nil {
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[api][SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
||||
}
|
||||
c.Set("Content-Type", "text/html")
|
||||
err = t.Execute(c, ui)
|
||||
if err != nil {
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[api][SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
return c.Status(500).SendString("Failed to parse template. This should never happen, because the template is validated on start.")
|
||||
}
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/TwiN/gatus/v5/storage/store"
|
||||
"github.com/TwiN/gatus/v5/watchdog"
|
||||
@@ -27,10 +30,14 @@ func TestSinglePageApplication(t *testing.T) {
|
||||
Group: "core",
|
||||
},
|
||||
},
|
||||
UI: &ui.Config{
|
||||
Title: "example-title",
|
||||
},
|
||||
}
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg)
|
||||
api := New(cfg)
|
||||
router := api.Router()
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
@@ -41,24 +48,31 @@ func TestSinglePageApplication(t *testing.T) {
|
||||
{
|
||||
Name: "frontend-home",
|
||||
Path: "/",
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedCode: 200,
|
||||
},
|
||||
{
|
||||
Name: "frontend-endpoint",
|
||||
Path: "/endpoints/core_frontend",
|
||||
ExpectedCode: http.StatusOK,
|
||||
ExpectedCode: 200,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
response, err := router.Test(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
|
||||
}
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
if !strings.Contains(string(body), cfg.UI.Title) {
|
||||
t.Errorf("%s %s should have contained the title", request.Method, request.URL)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/TwiN/gatus/v5/storage/store/common"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,9 +18,9 @@ const (
|
||||
MaximumPageSize = common.MaximumNumberOfResults
|
||||
)
|
||||
|
||||
func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) {
|
||||
func extractPageAndPageSizeFromRequest(c *fiber.Ctx) (page, pageSize int) {
|
||||
var err error
|
||||
if pageParameter := r.URL.Query().Get("page"); len(pageParameter) == 0 {
|
||||
if pageParameter := c.Query("page"); len(pageParameter) == 0 {
|
||||
page = DefaultPage
|
||||
} else {
|
||||
page, err = strconv.Atoi(pageParameter)
|
||||
@@ -31,7 +31,7 @@ func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int)
|
||||
page = DefaultPage
|
||||
}
|
||||
}
|
||||
if pageSizeParameter := r.URL.Query().Get("pageSize"); len(pageSizeParameter) == 0 {
|
||||
if pageSizeParameter := c.Query("pageSize"); len(pageSizeParameter) == 0 {
|
||||
pageSize = DefaultPageSize
|
||||
} else {
|
||||
pageSize, err = strconv.Atoi(pageSizeParameter)
|
||||
@@ -1,9 +1,11 @@
|
||||
package handler
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
|
||||
@@ -54,8 +56,12 @@ func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run("page-"+scenario.Page+"-pageSize-"+scenario.PageSize, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/statuses?page=%s&pageSize=%s", scenario.Page, scenario.PageSize), http.NoBody)
|
||||
actualPage, actualPageSize := extractPageAndPageSizeFromRequest(request)
|
||||
//request := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/statuses?page=%s&pageSize=%s", scenario.Page, scenario.PageSize), http.NoBody)
|
||||
app := fiber.New()
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
defer app.ReleaseCtx(c)
|
||||
c.Request().SetRequestURI(fmt.Sprintf("/api/v1/statuses?page=%s&pageSize=%s", scenario.Page, scenario.PageSize))
|
||||
actualPage, actualPageSize := extractPageAndPageSizeFromRequest(c)
|
||||
if actualPage != scenario.ExpectedPage {
|
||||
t.Errorf("expected %d, got %d", scenario.ExpectedPage, actualPage)
|
||||
}
|
||||
121
client/client.go
121
client/client.go
@@ -3,6 +3,7 @@ package client
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"github.com/TwiN/whois"
|
||||
"github.com/ishidawataru/sctp"
|
||||
ping "github.com/prometheus-community/pro-bing"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -142,26 +145,97 @@ func CanPerformStartTLS(address string, config *Config) (connected bool, certifi
|
||||
|
||||
// CanPerformTLS checks whether a connection can be established to an address using the TLS protocol
|
||||
func CanPerformTLS(address string, config *Config) (connected bool, certificate *x509.Certificate, err error) {
|
||||
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, nil)
|
||||
connection, err := tls.DialWithDialer(&net.Dialer{Timeout: config.Timeout}, "tcp", address, &tls.Config{
|
||||
InsecureSkipVerify: config.Insecure,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
verifiedChains := connection.ConnectionState().VerifiedChains
|
||||
// If config.Insecure is set to true, verifiedChains will be an empty list []
|
||||
// We should get the parsed certificates from PeerCertificates, it can't be empty on the client side
|
||||
// Reference: https://pkg.go.dev/crypto/tls#PeerCertificates
|
||||
if len(verifiedChains) == 0 || len(verifiedChains[0]) == 0 {
|
||||
return
|
||||
peerCertificates := connection.ConnectionState().PeerCertificates
|
||||
return true, peerCertificates[0], nil
|
||||
}
|
||||
return true, verifiedChains[0][0], nil
|
||||
}
|
||||
|
||||
// CanCreateSSHConnection checks whether a connection can be established and a command can be executed to an address
|
||||
// using the SSH protocol.
|
||||
func CanCreateSSHConnection(address, username, password string, config *Config) (bool, *ssh.Client, error) {
|
||||
var port string
|
||||
if strings.Contains(address, ":") {
|
||||
addressAndPort := strings.Split(address, ":")
|
||||
if len(addressAndPort) != 2 {
|
||||
return false, nil, errors.New("invalid address for ssh, format must be host:port")
|
||||
}
|
||||
address = addressAndPort[0]
|
||||
port = addressAndPort[1]
|
||||
} else {
|
||||
port = "22"
|
||||
}
|
||||
|
||||
cli, err := ssh.Dial("tcp", strings.Join([]string{address, port}, ":"), &ssh.ClientConfig{
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(password),
|
||||
},
|
||||
Timeout: config.Timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return true, cli, nil
|
||||
}
|
||||
|
||||
// ExecuteSSHCommand executes a command to an address using the SSH protocol.
|
||||
func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool, int, error) {
|
||||
type Body struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
defer sshClient.Close()
|
||||
|
||||
var b Body
|
||||
if err := json.Unmarshal([]byte(body), &b); err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
sess, err := sshClient.NewSession()
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
err = sess.Start(b.Command)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
defer sess.Close()
|
||||
|
||||
err = sess.Wait()
|
||||
if err == nil {
|
||||
return true, 0, nil
|
||||
}
|
||||
|
||||
e, ok := err.(*ssh.ExitError)
|
||||
if !ok {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
return true, e.ExitStatus(), nil
|
||||
}
|
||||
|
||||
// Ping checks if an address can be pinged and returns the round-trip time if the address can be pinged
|
||||
//
|
||||
// Note that this function takes at least 100ms, even if the address is 127.0.0.1
|
||||
func Ping(address string, config *Config) (bool, time.Duration) {
|
||||
pinger, err := ping.NewPinger(address)
|
||||
if err != nil {
|
||||
return false, 0
|
||||
}
|
||||
pinger := ping.New(address)
|
||||
pinger.Count = 1
|
||||
pinger.Timeout = config.Timeout
|
||||
// Set the pinger's privileged mode to true for every GOOS except darwin
|
||||
@@ -170,7 +244,8 @@ func Ping(address string, config *Config) (bool, time.Duration) {
|
||||
// Note that for this to work on Linux, Gatus must run with sudo privileges.
|
||||
// See https://github.com/prometheus-community/pro-bing#linux
|
||||
pinger.SetPrivileged(runtime.GOOS != "darwin")
|
||||
err = pinger.Run()
|
||||
pinger.SetNetwork(config.Network)
|
||||
err := pinger.Run()
|
||||
if err != nil {
|
||||
return false, 0
|
||||
}
|
||||
@@ -184,6 +259,38 @@ func Ping(address string, config *Config) (bool, time.Duration) {
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// QueryWebSocket opens a websocket connection, write `body` and return a message from the server
|
||||
func QueryWebSocket(address, body string, config *Config) (bool, []byte, error) {
|
||||
const (
|
||||
Origin = "http://localhost/"
|
||||
MaximumMessageSize = 1024 // in bytes
|
||||
)
|
||||
wsConfig, err := websocket.NewConfig(address, Origin)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("error configuring websocket connection: %w", err)
|
||||
}
|
||||
if config != nil {
|
||||
wsConfig.Dialer = &net.Dialer{Timeout: config.Timeout}
|
||||
}
|
||||
// Dial URL
|
||||
ws, err := websocket.DialConfig(wsConfig)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
|
||||
}
|
||||
defer ws.Close()
|
||||
// Write message
|
||||
if _, err := ws.Write([]byte(body)); err != nil {
|
||||
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
|
||||
}
|
||||
// Read message
|
||||
var n int
|
||||
msg := make([]byte, MaximumMessageSize)
|
||||
if n, err = ws.Read(msg); err != nil {
|
||||
return false, nil, fmt.Errorf("error reading websocket message: %w", err)
|
||||
}
|
||||
return true, msg[:n], nil
|
||||
}
|
||||
|
||||
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
|
||||
func InjectHTTPClient(httpClient *http.Client) {
|
||||
injectedHTTPClient = httpClient
|
||||
|
||||
@@ -83,6 +83,32 @@ func TestPing(t *testing.T) {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
// Can't perform integration tests (e.g. pinging public targets by single-stacked hostname) here,
|
||||
// because ICMP is blocked in the network of GitHub-hosted runners.
|
||||
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
||||
t.Error("expected true")
|
||||
if rtt == 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
|
||||
t.Error("expected true")
|
||||
if rtt == 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip4"}); success {
|
||||
t.Error("expected false, because the IP isn't an IPv4 address")
|
||||
if rtt != 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip6"}); success {
|
||||
t.Error("expected false, because the IP isn't an IPv6 address")
|
||||
if rtt != 0 {
|
||||
t.Error("Round-trip time returned on failure should've been 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanPerformStartTLS(t *testing.T) {
|
||||
@@ -191,6 +217,9 @@ func TestCanCreateTCPConnection(t *testing.T) {
|
||||
if CanCreateTCPConnection("127.0.0.1", &Config{Timeout: 5 * time.Second}) {
|
||||
t.Error("should've failed, because there's no port in the address")
|
||||
}
|
||||
if !CanCreateTCPConnection("1.1.1.1:53", &Config{Timeout: 5 * time.Second}) {
|
||||
t.Error("should've succeeded, because that IP should always™ be up")
|
||||
}
|
||||
}
|
||||
|
||||
// This test checks if a HTTP client configured with `configureOAuth2()` automatically
|
||||
@@ -250,3 +279,14 @@ func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
||||
t.Error("exptected `secret-token` as Bearer token in the mocked response header `X-Org-Authorization`, but got", response.Header.Get("X-Org-Authorization"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryWebSocket(t *testing.T) {
|
||||
_, _, err := QueryWebSocket("", "body", &Config{Timeout: 2 * time.Second})
|
||||
if err == nil {
|
||||
t.Error("expected an error due to the address being invalid")
|
||||
}
|
||||
_, _, err = QueryWebSocket("ws://example.org", "body", &Config{Timeout: 2 * time.Second})
|
||||
if err == nil {
|
||||
t.Error("expected an error due to the target not being websocket-friendly")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,31 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"google.golang.org/api/idtoken"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPTimeout = 10 * time.Second
|
||||
defaultTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDNSResolver = errors.New("invalid DNS resolver specified. Required format is {proto}://{ip}:{port}")
|
||||
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
|
||||
ErrInvalidClientOAuth2Config = errors.New("invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)")
|
||||
ErrInvalidClientIAPConfig = errors.New("invalid Identity-Aware-Proxy configuration: must define all fields for Google Identity-Aware-Proxy programmatic authentication (audience)")
|
||||
|
||||
defaultConfig = Config{
|
||||
Insecure: false,
|
||||
IgnoreRedirect: false,
|
||||
Timeout: defaultHTTPTimeout,
|
||||
Timeout: defaultTimeout,
|
||||
Network: "ip",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,6 +43,9 @@ func GetDefaultConfig() *Config {
|
||||
|
||||
// Config is the configuration for clients
|
||||
type Config struct {
|
||||
// ProxyURL is the URL of the proxy to use for the client
|
||||
ProxyURL string `yaml:"proxy-url,omitempty"`
|
||||
|
||||
// Insecure determines whether to skip verifying the server's certificate chain and host name
|
||||
Insecure bool `yaml:"insecure,omitempty"`
|
||||
|
||||
@@ -58,7 +65,13 @@ type Config struct {
|
||||
// See configureOAuth2 for more details.
|
||||
OAuth2Config *OAuth2Config `yaml:"oauth2,omitempty"`
|
||||
|
||||
// IAPConfig is the Google Cloud Identity-Aware-Proxy configuration used for the client. (e.g. audience)
|
||||
IAPConfig *IAPConfig `yaml:"identity-aware-proxy,omitempty"`
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
// Network (ip, ip4 or ip6) for the ICMP client
|
||||
Network string `yaml:"network"`
|
||||
}
|
||||
|
||||
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
|
||||
@@ -76,6 +89,11 @@ type OAuth2Config struct {
|
||||
Scopes []string `yaml:"scopes"` // e.g. ["openid"]
|
||||
}
|
||||
|
||||
// IAPConfig is the configuration for the Google Cloud Identity-Aware-Proxy
|
||||
type IAPConfig struct {
|
||||
Audience string `yaml:"audience"` // e.g. "toto.apps.googleusercontent.com"
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
|
||||
func (c *Config) ValidateAndSetDefaults() error {
|
||||
if c.Timeout < time.Millisecond {
|
||||
@@ -90,6 +108,9 @@ func (c *Config) ValidateAndSetDefaults() error {
|
||||
if c.HasOAuth2Config() && !c.OAuth2Config.isValid() {
|
||||
return ErrInvalidClientOAuth2Config
|
||||
}
|
||||
if c.HasIAPConfig() && !c.IAPConfig.isValid() {
|
||||
return ErrInvalidClientIAPConfig
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -130,6 +151,16 @@ func (c *Config) HasOAuth2Config() bool {
|
||||
return c.OAuth2Config != nil
|
||||
}
|
||||
|
||||
// HasIAPConfig returns true if the client has IAP configuration parameters
|
||||
func (c *Config) HasIAPConfig() bool {
|
||||
return c.IAPConfig != nil
|
||||
}
|
||||
|
||||
// isValid() returns true if the IAP configuration is valid
|
||||
func (c *IAPConfig) isValid() bool {
|
||||
return len(c.Audience) > 0
|
||||
}
|
||||
|
||||
// isValid() returns true if the OAuth2 configuration is valid
|
||||
func (c *OAuth2Config) isValid() bool {
|
||||
return len(c.TokenURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
|
||||
@@ -157,6 +188,14 @@ func (c *Config) getHTTPClient() *http.Client {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if c.ProxyURL != "" {
|
||||
proxyURL, err := url.Parse(c.ProxyURL)
|
||||
if err != nil {
|
||||
log.Println("[client][getHTTPClient] THIS SHOULD NOT HAPPEN. Silently ignoring custom proxy due to error:", err.Error())
|
||||
} else {
|
||||
c.httpClient.Transport.(*http.Transport).Proxy = http.ProxyURL(proxyURL)
|
||||
}
|
||||
}
|
||||
if c.HasCustomDNSResolver() {
|
||||
dnsResolver, err := c.parseDNSResolver()
|
||||
if err != nil {
|
||||
@@ -178,13 +217,56 @@ func (c *Config) getHTTPClient() *http.Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.HasOAuth2Config() {
|
||||
if c.HasOAuth2Config() && c.HasIAPConfig() {
|
||||
log.Println("[client][getHTTPClient] Error: Both Identity-Aware-Proxy and Oauth2 configuration are present.")
|
||||
} else if c.HasOAuth2Config() {
|
||||
c.httpClient = configureOAuth2(c.httpClient, *c.OAuth2Config)
|
||||
} else if c.HasIAPConfig() {
|
||||
c.httpClient = configureIAP(c.httpClient, *c.IAPConfig)
|
||||
}
|
||||
}
|
||||
return c.httpClient
|
||||
}
|
||||
|
||||
// validateIAPToken returns a boolean that will define if the google identity-aware-proxy token can be fetch
|
||||
// and if is it valid.
|
||||
func validateIAPToken(ctx context.Context, c IAPConfig) bool {
|
||||
ts, err := idtoken.NewTokenSource(ctx, c.Audience)
|
||||
if err != nil {
|
||||
log.Println("[client][ValidateIAPToken] Claiming Identity token failed. error:", err.Error())
|
||||
return false
|
||||
}
|
||||
tok, err := ts.Token()
|
||||
if err != nil {
|
||||
log.Println("[client][ValidateIAPToken] Get Identity-Aware-Proxy token failed. error:", err.Error())
|
||||
return false
|
||||
}
|
||||
payload, err := idtoken.Validate(ctx, tok.AccessToken, c.Audience)
|
||||
_ = payload
|
||||
if err != nil {
|
||||
log.Println("[client][ValidateIAPToken] Token Validation failed. error:", err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// configureIAP returns an HTTP client that will obtain and refresh Identity-Aware-Proxy tokens as necessary.
|
||||
// The returned Client and its Transport should not be modified.
|
||||
func configureIAP(httpClient *http.Client, c IAPConfig) *http.Client {
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||
if validateIAPToken(ctx, c) {
|
||||
ts, err := idtoken.NewTokenSource(ctx, c.Audience)
|
||||
if err != nil {
|
||||
log.Println("[client][ConfigureIAP] Claiming Token Source failed. error:", err.Error())
|
||||
return httpClient
|
||||
}
|
||||
client := oauth2.NewClient(ctx, ts)
|
||||
client.Timeout = httpClient.Timeout
|
||||
return client
|
||||
}
|
||||
return httpClient
|
||||
}
|
||||
|
||||
// configureOAuth2 returns an HTTP client that will obtain and refresh tokens as necessary.
|
||||
// The returned Client and its Transport should not be modified.
|
||||
func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
||||
@@ -195,5 +277,7 @@ func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
|
||||
TokenURL: c.TokenURL,
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||
return oauth2cfg.Client(ctx)
|
||||
client := oauth2cfg.Client(ctx)
|
||||
client.Timeout = httpClient.Timeout
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -13,7 +14,7 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
||||
if !(insecureClient.Transport).(*http.Transport).TLSClientConfig.InsecureSkipVerify {
|
||||
t.Error("expected Config.Insecure set to true to cause the HTTP client to skip certificate verification")
|
||||
}
|
||||
if insecureClient.Timeout != defaultHTTPTimeout {
|
||||
if insecureClient.Timeout != defaultTimeout {
|
||||
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
|
||||
}
|
||||
request, _ := http.NewRequest("GET", "", nil)
|
||||
@@ -79,3 +80,29 @@ func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_getHTTPClient_withCustomProxyURL(t *testing.T) {
|
||||
proxyURL := "http://proxy.example.com:8080"
|
||||
cfg := &Config{
|
||||
ProxyURL: proxyURL,
|
||||
}
|
||||
cfg.ValidateAndSetDefaults()
|
||||
client := cfg.getHTTPClient()
|
||||
transport := client.Transport.(*http.Transport)
|
||||
if transport.Proxy == nil {
|
||||
t.Errorf("expected Config.ProxyURL to set the HTTP client's proxy to %s", proxyURL)
|
||||
}
|
||||
req := &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "www.example.com",
|
||||
},
|
||||
}
|
||||
expectProxyURL, err := transport.Proxy(req)
|
||||
if err != nil {
|
||||
t.Errorf("can't proxy the request %s", proxyURL)
|
||||
}
|
||||
if proxyURL != expectProxyURL.String() {
|
||||
t.Errorf("expected Config.ProxyURL to set the HTTP client's proxy to %s", proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/alerting"
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||
"github.com/TwiN/gatus/v5/config/remote"
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
@@ -91,6 +92,9 @@ type Config struct {
|
||||
// WARNING: This is in ALPHA and may change or be completely removed in the future
|
||||
Remote *remote.Config `yaml:"remote,omitempty"`
|
||||
|
||||
// Connectivity is the configuration for connectivity
|
||||
Connectivity *connectivity.Config `yaml:"connectivity,omitempty"`
|
||||
|
||||
configPath string // path to the file or directory from which config was loaded
|
||||
lastFileModTime time.Time // last modification time
|
||||
}
|
||||
@@ -252,10 +256,20 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||
if err := validateRemoteConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateConnectivityConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func validateConnectivityConfig(config *Config) error {
|
||||
if config.Connectivity != nil {
|
||||
return config.Connectivity.ValidateAndSetDefaults()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemoteConfig(config *Config) error {
|
||||
if config.Remote != nil {
|
||||
if err := config.Remote.ValidateAndSetDefaults(); err != nil {
|
||||
@@ -347,10 +361,13 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
||||
return
|
||||
}
|
||||
alertTypes := []alert.Type{
|
||||
alert.TypeAWSSES,
|
||||
alert.TypeCustom,
|
||||
alert.TypeDiscord,
|
||||
alert.TypeGitHub,
|
||||
alert.TypeGitLab,
|
||||
alert.TypeGoogleChat,
|
||||
alert.TypeGotify,
|
||||
alert.TypeEmail,
|
||||
alert.TypeMatrix,
|
||||
alert.TypeMattermost,
|
||||
|
||||
53
config/connectivity/connectivity.go
Normal file
53
config/connectivity/connectivity.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidInterval = errors.New("connectivity.checker.interval must be 5s or higher")
|
||||
ErrInvalidDNSTarget = errors.New("connectivity.checker.target must be suffixed with :53")
|
||||
)
|
||||
|
||||
// Config is the configuration for the connectivity checker.
|
||||
type Config struct {
|
||||
Checker *Checker `yaml:"checker,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) ValidateAndSetDefaults() error {
|
||||
if c.Checker != nil {
|
||||
if c.Checker.Interval == 0 {
|
||||
c.Checker.Interval = 60 * time.Second
|
||||
} else if c.Checker.Interval < 5*time.Second {
|
||||
return ErrInvalidInterval
|
||||
}
|
||||
if !strings.HasSuffix(c.Checker.Target, ":53") {
|
||||
return ErrInvalidDNSTarget
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checker is the configuration for making sure Gatus has access to the internet.
|
||||
type Checker struct {
|
||||
Target string `yaml:"target"` // e.g. 1.1.1.1:53
|
||||
Interval time.Duration `yaml:"interval,omitempty"`
|
||||
|
||||
isConnected bool
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
func (c Checker) Check() bool {
|
||||
return client.CanCreateTCPConnection(c.Target, &client.Config{Timeout: 5 * time.Second})
|
||||
}
|
||||
|
||||
func (c *Checker) IsConnected() bool {
|
||||
if now := time.Now(); now.After(c.lastCheck.Add(c.Interval)) {
|
||||
c.lastCheck, c.isConnected = now, c.Check()
|
||||
}
|
||||
return c.isConnected
|
||||
}
|
||||
62
config/connectivity/connectivity_test.go
Normal file
62
config/connectivity/connectivity_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package connectivity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expectedErr error
|
||||
expectedInterval time.Duration
|
||||
}{
|
||||
{
|
||||
name: "good-config",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}},
|
||||
expectedInterval: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "good-config-with-default-interval",
|
||||
cfg: &Config{Checker: &Checker{Target: "8.8.8.8:53", Interval: 0}},
|
||||
expectedInterval: 60 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "config-with-interval-too-low",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:53", Interval: 4 * time.Second}},
|
||||
expectedErr: ErrInvalidInterval,
|
||||
},
|
||||
{
|
||||
name: "config-with-invalid-target-due-to-missing-port",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1", Interval: 15 * time.Second}},
|
||||
expectedErr: ErrInvalidDNSTarget,
|
||||
},
|
||||
{
|
||||
name: "config-with-invalid-target-due-to-invalid-dns-port",
|
||||
cfg: &Config{Checker: &Checker{Target: "1.1.1.1:52", Interval: 15 * time.Second}},
|
||||
expectedErr: ErrInvalidDNSTarget,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
err := scenario.cfg.ValidateAndSetDefaults()
|
||||
if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", scenario.expectedErr) {
|
||||
t.Errorf("expected error %v, got %v", scenario.expectedErr, err)
|
||||
}
|
||||
if err == nil && scenario.expectedErr == nil {
|
||||
if scenario.cfg.Checker.Interval != scenario.expectedInterval {
|
||||
t.Errorf("expected interval %v, got %v", scenario.expectedInterval, scenario.cfg.Checker.Interval)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecker_IsConnected(t *testing.T) {
|
||||
checker := &Checker{Target: "1.1.1.1:53", Interval: 10 * time.Second}
|
||||
if !checker.IsConnected() {
|
||||
t.Error("expected checker.IsConnected() to be true")
|
||||
}
|
||||
}
|
||||
1
config/endpoints/README.md
Normal file
1
config/endpoints/README.md
Normal file
@@ -0,0 +1 @@
|
||||
TODO: move files from core to here.
|
||||
@@ -82,7 +82,7 @@ func (c *Config) ValidateAndSetDefaults() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Duration <= 0 || c.Duration >= 24*time.Hour {
|
||||
if c.Duration <= 0 || c.Duration > 24*time.Hour {
|
||||
return errInvalidMaintenanceDuration
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -211,6 +211,14 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-22h-ago-for-24h",
|
||||
cfg: &Config{
|
||||
Start: fmt.Sprintf("%02d:00", normalizeHour(now.Hour()-22)),
|
||||
Duration: 24 * time.Hour,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "under-maintenance-starting-4h-ago-for-3h",
|
||||
cfg: &Config{
|
||||
@@ -236,6 +244,15 @@ func TestConfig_IsUnderMaintenance(t *testing.T) {
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "not-under-maintenance-today-with-24h-duration",
|
||||
cfg: &Config{
|
||||
Start: fmt.Sprintf("%02d:00", now.Hour()),
|
||||
Duration: 24 * time.Hour,
|
||||
Every: []string{now.Add(48 * time.Hour).Weekday().String()},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
@@ -11,21 +13,50 @@ const (
|
||||
|
||||
// DefaultPort is the default port the application will listen on
|
||||
DefaultPort = 8080
|
||||
|
||||
// DefaultReadBufferSize is the default value for ReadBufferSize
|
||||
DefaultReadBufferSize = 8192
|
||||
|
||||
// MinimumReadBufferSize is the minimum value for ReadBufferSize, and also the default value set
|
||||
// for fiber.Config.ReadBufferSize
|
||||
MinimumReadBufferSize = 4096
|
||||
)
|
||||
|
||||
// Config is the structure which supports the configuration of the endpoint
|
||||
// which provides access to the web frontend
|
||||
// Config is the structure which supports the configuration of the server listening to requests
|
||||
type Config struct {
|
||||
// Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress)
|
||||
Address string `yaml:"address"`
|
||||
|
||||
// Port to listen on (default to 8080 specified by DefaultPort)
|
||||
Port int `yaml:"port"`
|
||||
|
||||
// ReadBufferSize sets fiber.Config.ReadBufferSize, which is the buffer size for reading requests coming from a
|
||||
// single connection and also acts as a limit for the maximum header size.
|
||||
//
|
||||
// If you're getting occasional "Request Header Fields Too Large", you may want to try increasing this value.
|
||||
//
|
||||
// Defaults to DefaultReadBufferSize
|
||||
ReadBufferSize int `yaml:"read-buffer-size,omitempty"`
|
||||
|
||||
// TLS configuration (optional)
|
||||
TLS *TLSConfig `yaml:"tls,omitempty"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
// CertificateFile is the public certificate for TLS in PEM format.
|
||||
CertificateFile string `yaml:"certificate-file,omitempty"`
|
||||
|
||||
// PrivateKeyFile is the private key file for TLS in PEM format.
|
||||
PrivateKeyFile string `yaml:"private-key-file,omitempty"`
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns a Config struct with the default values
|
||||
func GetDefaultConfig() *Config {
|
||||
return &Config{Address: DefaultAddress, Port: DefaultPort}
|
||||
return &Config{
|
||||
Address: DefaultAddress,
|
||||
Port: DefaultPort,
|
||||
ReadBufferSize: DefaultReadBufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the web configuration and sets the default values if necessary.
|
||||
@@ -40,10 +71,37 @@ func (web *Config) ValidateAndSetDefaults() error {
|
||||
} else if web.Port < 0 || web.Port > math.MaxUint16 {
|
||||
return fmt.Errorf("invalid port: value should be between %d and %d", 0, math.MaxUint16)
|
||||
}
|
||||
// Validate ReadBufferSize
|
||||
if web.ReadBufferSize == 0 {
|
||||
web.ReadBufferSize = DefaultReadBufferSize // Not set? Use the default value.
|
||||
} else if web.ReadBufferSize < MinimumReadBufferSize {
|
||||
web.ReadBufferSize = MinimumReadBufferSize // Below the minimum? Use the minimum value.
|
||||
}
|
||||
// Try to load the TLS certificates
|
||||
if web.TLS != nil {
|
||||
if err := web.TLS.isValid(); err != nil {
|
||||
return fmt.Errorf("invalid tls config: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (web *Config) HasTLS() bool {
|
||||
return web.TLS != nil && len(web.TLS.CertificateFile) > 0 && len(web.TLS.PrivateKeyFile) > 0
|
||||
}
|
||||
|
||||
// SocketAddress returns the combination of the Address and the Port
|
||||
func (web *Config) SocketAddress() string {
|
||||
return fmt.Sprintf("%s:%d", web.Address, web.Port)
|
||||
}
|
||||
|
||||
func (t *TLSConfig) isValid() error {
|
||||
if len(t.CertificateFile) > 0 && len(t.PrivateKeyFile) > 0 {
|
||||
_, err := tls.LoadX509KeyPair(t.CertificateFile, t.PrivateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("certificate-file and private-key-file must be specified")
|
||||
}
|
||||
|
||||
@@ -12,28 +12,84 @@ func TestGetDefaultConfig(t *testing.T) {
|
||||
if defaultConfig.Address != DefaultAddress {
|
||||
t.Error("expected default config to have the default address")
|
||||
}
|
||||
if defaultConfig.ReadBufferSize != DefaultReadBufferSize {
|
||||
t.Error("expected default config to have the default read buffer size")
|
||||
}
|
||||
if defaultConfig.TLS != nil {
|
||||
t.Error("expected default config to have TLS disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expectedAddress string
|
||||
expectedPort int
|
||||
expectedErr bool
|
||||
name string
|
||||
cfg *Config
|
||||
expectedAddress string
|
||||
expectedPort int
|
||||
expectedReadBufferSize int
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "no-explicit-config",
|
||||
cfg: &Config{},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 8080,
|
||||
expectedErr: false,
|
||||
name: "no-explicit-config",
|
||||
cfg: &Config{},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 8080,
|
||||
expectedReadBufferSize: 8192,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid-port",
|
||||
cfg: &Config{Port: 100000000},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "read-buffer-size-below-minimum",
|
||||
cfg: &Config{ReadBufferSize: 1024},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 8080,
|
||||
expectedReadBufferSize: MinimumReadBufferSize, // minimum is 4096, default is 8192.
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "read-buffer-size-at-minimum",
|
||||
cfg: &Config{ReadBufferSize: MinimumReadBufferSize},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 8080,
|
||||
expectedReadBufferSize: 4096,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "custom-read-buffer-size",
|
||||
cfg: &Config{ReadBufferSize: 65536},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 8080,
|
||||
expectedReadBufferSize: 65536,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "with-good-tls-config",
|
||||
cfg: &Config{Port: 443, TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 443,
|
||||
expectedReadBufferSize: 8192,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "with-bad-tls-config",
|
||||
cfg: &Config{Port: 443, TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 443,
|
||||
expectedReadBufferSize: 8192,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "with-partial-tls-config",
|
||||
cfg: &Config{Port: 443, TLS: &TLSConfig{CertificateFile: "", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedAddress: "0.0.0.0",
|
||||
expectedPort: 443,
|
||||
expectedReadBufferSize: 8192,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
@@ -44,10 +100,13 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||
}
|
||||
if !scenario.expectedErr {
|
||||
if scenario.cfg.Port != scenario.expectedPort {
|
||||
t.Errorf("expected port to be %d, got %d", scenario.expectedPort, scenario.cfg.Port)
|
||||
t.Errorf("expected Port to be %d, got %d", scenario.expectedPort, scenario.cfg.Port)
|
||||
}
|
||||
if scenario.cfg.ReadBufferSize != scenario.expectedReadBufferSize {
|
||||
t.Errorf("expected ReadBufferSize to be %d, got %d", scenario.expectedReadBufferSize, scenario.cfg.ReadBufferSize)
|
||||
}
|
||||
if scenario.cfg.Address != scenario.expectedAddress {
|
||||
t.Errorf("expected address to be %s, got %s", scenario.expectedAddress, scenario.cfg.Address)
|
||||
t.Errorf("expected Address to be %s, got %s", scenario.expectedAddress, scenario.cfg.Address)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -63,3 +122,66 @@ func TestConfig_SocketAddress(t *testing.T) {
|
||||
t.Errorf("expected %s, got %s", "0.0.0.0:8081", web.SocketAddress())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_isValid(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "good-tls-config",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing-certificate-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "doesnotexist", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-certificate-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "no-certificate-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "", PrivateKeyFile: "../../testdata/cert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "doesnotexist"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "no-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: ""}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/cert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad-certificate-and-private-key-file",
|
||||
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../../testdata/badcert.pem", PrivateKeyFile: "../../testdata/badcert.key"}},
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
err := scenario.cfg.ValidateAndSetDefaults()
|
||||
if (err != nil) != scenario.expectedErr {
|
||||
t.Errorf("expected the existence of an error to be %v, got %v", scenario.expectedErr, err)
|
||||
return
|
||||
}
|
||||
if !scenario.expectedErr {
|
||||
if scenario.cfg.TLS.isValid() != nil {
|
||||
t.Error("cfg.TLS.isValid() returned an error even though no error was expected")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v5/api"
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/controller/handler"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// server is the http.Server created by Handle.
|
||||
// The only reason it exists is for testing purposes.
|
||||
server *http.Server
|
||||
app *fiber.App
|
||||
)
|
||||
|
||||
// Handle creates the router and starts the server
|
||||
func Handle(cfg *config.Config) {
|
||||
var router http.Handler = handler.CreateRouter(cfg)
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
router = handler.DevelopmentCORS(router)
|
||||
}
|
||||
server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
log.Println("[controller][Handle] Listening on " + cfg.Web.SocketAddress())
|
||||
api := api.New(cfg)
|
||||
app = api.Router()
|
||||
server := app.Server()
|
||||
server.ReadTimeout = 15 * time.Second
|
||||
server.WriteTimeout = 15 * time.Second
|
||||
server.IdleTimeout = 15 * time.Second
|
||||
if os.Getenv("ROUTER_TEST") == "true" {
|
||||
return
|
||||
}
|
||||
log.Println("[controller][Handle]", server.ListenAndServe())
|
||||
log.Println("[controller][Handle] Listening on " + cfg.Web.SocketAddress())
|
||||
if cfg.Web.HasTLS() {
|
||||
err := app.ListenTLS(cfg.Web.SocketAddress(), cfg.Web.TLS.CertificateFile, cfg.Web.TLS.PrivateKeyFile)
|
||||
if err != nil {
|
||||
log.Fatal("[controller][Handle]", err)
|
||||
}
|
||||
} else {
|
||||
err := app.Listen(cfg.Web.SocketAddress())
|
||||
if err != nil {
|
||||
log.Fatal("[controller][Handle]", err)
|
||||
}
|
||||
}
|
||||
log.Println("[controller][Handle] Server has shut down successfully")
|
||||
}
|
||||
|
||||
// Shutdown stops the server
|
||||
func Shutdown() {
|
||||
if server != nil {
|
||||
_ = server.Shutdown(context.TODO())
|
||||
server = nil
|
||||
if app != nil {
|
||||
_ = app.Shutdown()
|
||||
app = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
"github.com/TwiN/gatus/v5/config/web"
|
||||
"github.com/TwiN/gatus/v5/core"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TestHandle(t *testing.T) {
|
||||
@@ -34,22 +35,68 @@ func TestHandle(t *testing.T) {
|
||||
defer os.Clearenv()
|
||||
Handle(cfg)
|
||||
defer Shutdown()
|
||||
request, _ := http.NewRequest("GET", "/health", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusOK {
|
||||
request := httptest.NewRequest("GET", "/health", http.NoBody)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
t.Error("expected GET /health to return status code 200")
|
||||
}
|
||||
if server == nil {
|
||||
if app == nil {
|
||||
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTLS(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
tls *web.TLSConfig
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "good-tls-config",
|
||||
tls: &web.TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
|
||||
expectedStatusCode: 200,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Web: &web.Config{Address: "0.0.0.0", Port: rand.Intn(65534), TLS: scenario.tls},
|
||||
Endpoints: []*core.Endpoint{
|
||||
{Name: "frontend", Group: "core"},
|
||||
{Name: "backend", Group: "core"},
|
||||
},
|
||||
}
|
||||
if err := cfg.Web.ValidateAndSetDefaults(); err != nil {
|
||||
t.Error("expected no error from web (TLS) validation, got", err)
|
||||
}
|
||||
_ = os.Setenv("ROUTER_TEST", "true")
|
||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||
defer os.Clearenv()
|
||||
Handle(cfg)
|
||||
defer Shutdown()
|
||||
request := httptest.NewRequest("GET", "/health", http.NoBody)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.StatusCode != scenario.expectedStatusCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.expectedStatusCode, response.StatusCode)
|
||||
}
|
||||
if app == nil {
|
||||
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShutdown(t *testing.T) {
|
||||
// Pretend that we called controller.Handle(), which initializes the server variable
|
||||
server = &http.Server{}
|
||||
app = fiber.New()
|
||||
Shutdown()
|
||||
if server != nil {
|
||||
if app != nil {
|
||||
t.Error("server should've been shut down")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v5/security"
|
||||
)
|
||||
|
||||
// ConfigHandler is a handler that returns information for the front end of the application.
|
||||
type ConfigHandler struct {
|
||||
securityConfig *security.Config
|
||||
}
|
||||
|
||||
func (handler ConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
hasOIDC := false
|
||||
isAuthenticated := true // Default to true if no security config is set
|
||||
if handler.securityConfig != nil {
|
||||
hasOIDC = handler.securityConfig.OIDC != nil
|
||||
isAuthenticated = handler.securityConfig.IsAuthenticated(r)
|
||||
}
|
||||
// Return the config
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(fmt.Sprintf(`{"oidc":%v,"authenticated":%v}`, hasOIDC, isAuthenticated)))
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package handler
|
||||
|
||||
import "net/http"
|
||||
|
||||
func DevelopmentCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8081")
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var gzPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return gzip.NewWriter(io.Discard)
|
||||
},
|
||||
}
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
// WriteHeader sends an HTTP response header with the provided status code.
|
||||
// It also deletes the Content-Length header, since the GZIP compression may modify the size of the payload
|
||||
func (w *gzipResponseWriter) WriteHeader(status int) {
|
||||
w.Header().Del("Content-Length")
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
// Write writes len(b) bytes from b to the underlying data stream.
|
||||
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
// GzipHandler compresses the response of a given http.Handler if the request's headers specify that the client
|
||||
// supports gzip encoding
|
||||
func GzipHandler(next http.Handler) http.Handler {
|
||||
return GzipHandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
||||
next.ServeHTTP(writer, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GzipHandlerFunc compresses the response of a given http.HandlerFunc if the request's headers specify that the client
|
||||
// supports gzip encoding
|
||||
func GzipHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, r *http.Request) {
|
||||
// If the request doesn't specify that it supports gzip, then don't compress it
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
next.ServeHTTP(writer, r)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzPool.Get().(*gzip.Writer)
|
||||
defer gzPool.Put(gz)
|
||||
gz.Reset(writer)
|
||||
defer gz.Close()
|
||||
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: writer, Writer: gz}, r)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
static "github.com/TwiN/gatus/v5/web"
|
||||
"github.com/TwiN/health"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
if cfg.Metrics {
|
||||
router.Handle("/metrics", promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
|
||||
DisableCompression: true,
|
||||
}))).Methods("GET")
|
||||
}
|
||||
router.Use(GzipHandler)
|
||||
api := router.PathPrefix("/api").Subrouter()
|
||||
protected := api.PathPrefix("/").Subrouter()
|
||||
unprotected := api.PathPrefix("/").Subrouter()
|
||||
if cfg.Security != nil {
|
||||
if err := cfg.Security.RegisterHandlers(router); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cfg.Security.ApplySecurityMiddleware(protected); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
// Endpoints
|
||||
unprotected.Handle("/v1/config", ConfigHandler{securityConfig: cfg.Security}).Methods("GET")
|
||||
protected.HandleFunc("/v1/endpoints/statuses", EndpointStatuses(cfg)).Methods("GET")
|
||||
protected.HandleFunc("/v1/endpoints/{key}/statuses", EndpointStatus).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/health/badge.svg", HealthBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge(cfg)).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET")
|
||||
// Misc
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
// SPA
|
||||
router.HandleFunc("/endpoints/{name}", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||
router.HandleFunc("/", SinglePageApplication(cfg.UI)).Methods("GET")
|
||||
// Everything else falls back on static content
|
||||
staticFileSystem, err := fs.Sub(static.FileSystem, static.RootPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
router.PathPrefix("/").Handler(http.FileServer(http.FS(staticFileSystem)))
|
||||
return router
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config"
|
||||
)
|
||||
|
||||
func TestCreateRouter(t *testing.T) {
|
||||
router := CreateRouter(&config.Config{Metrics: true})
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
ExpectedCode int
|
||||
Gzip bool
|
||||
}
|
||||
scenarios := []Scenario{
|
||||
{
|
||||
Name: "health",
|
||||
Path: "/health",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Path: "/metrics",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "favicon.ico",
|
||||
Path: "/favicon.ico",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "app.js",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "app.js-gzipped",
|
||||
Path: "/js/app.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "chunk-vendors.js-gzipped",
|
||||
Path: "/js/chunk-vendors.js",
|
||||
ExpectedCode: http.StatusOK,
|
||||
Gzip: true,
|
||||
},
|
||||
{
|
||||
Name: "index-redirect",
|
||||
Path: "/index.html",
|
||||
ExpectedCode: http.StatusMovedPermanently,
|
||||
},
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.Name, func(t *testing.T) {
|
||||
request, _ := http.NewRequest("GET", scenario.Path, http.NoBody)
|
||||
if scenario.Gzip {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != scenario.ExpectedCode {
|
||||
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/gatus/v5/config/ui"
|
||||
static "github.com/TwiN/gatus/v5/web"
|
||||
)
|
||||
|
||||
func SinglePageApplication(ui *ui.Config) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
|
||||
if err != nil {
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[handler][SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
http.Error(writer, "Failed to parse template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html")
|
||||
err = t.Execute(writer, ui)
|
||||
if err != nil {
|
||||
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
|
||||
log.Println("[handler][SinglePageApplication] Failed to execute template. This should never happen, because the template is validated on start. Error:", err.Error())
|
||||
http.Error(writer, "Failed to execute template. This should never happen, because the template is validated on start.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +224,14 @@ func isEqual(first, second string) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// test if inputs are integers
|
||||
firstInt, err1 := strconv.ParseInt(first, 0, 64)
|
||||
secondInt, err2 := strconv.ParseInt(second, 0, 64)
|
||||
if err1 == nil && err2 == nil {
|
||||
return firstInt == secondInt
|
||||
}
|
||||
|
||||
return first == second
|
||||
}
|
||||
|
||||
@@ -304,7 +312,7 @@ func sanitizeAndResolveNumerical(list []string, result *Result) (parameters []st
|
||||
if duration, err := time.ParseDuration(element); duration != 0 && err == nil {
|
||||
// If the string is a duration, convert it to milliseconds
|
||||
resolvedNumericalParameters = append(resolvedNumericalParameters, duration.Milliseconds())
|
||||
} else if number, err := strconv.ParseInt(element, 10, 64); err != nil {
|
||||
} else if number, err := strconv.ParseInt(element, 0, 64); err != nil {
|
||||
// It's not an int, so we'll check if it's a float
|
||||
if f, err := strconv.ParseFloat(element, 64); err == nil {
|
||||
// It's a float, but we'll convert it to an int. We're losing precision here, but it's better than
|
||||
|
||||
@@ -287,6 +287,111 @@ func TestCondition_evaluate(t *testing.T) {
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data.id > 0",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-greater-than",
|
||||
Condition: Condition("[BODY].data > 0"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data > 0",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0x1",
|
||||
Condition: Condition("[BODY].data == 1"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 1",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-equals",
|
||||
Condition: Condition("[BODY].data == 0x1"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 0x1",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0x2",
|
||||
Condition: Condition("[BODY].data == 2"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x2\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 2",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0xF",
|
||||
Condition: Condition("[BODY].data == 15"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0xF\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 15",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-using-equal-to-0xC0ff33",
|
||||
Condition: Condition("[BODY].data == 12648243"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0xC0ff33\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 12648243",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-len",
|
||||
Condition: Condition("len([BODY].data) == 3"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "len([BODY].data) == 3",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-greater",
|
||||
Condition: Condition("[BODY].data >= 1"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x01\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data >= 1",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-hexadecimal-int-0x01-len",
|
||||
Condition: Condition("len([BODY].data) == 4"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0x01\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "len([BODY].data) == 4",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-octal-int-using-greater-than",
|
||||
Condition: Condition("[BODY].data > 0"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0o1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data > 0",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-octal-int-using-equal",
|
||||
Condition: Condition("[BODY].data == 2"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0o2\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 2",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-octal-int-using-equals",
|
||||
Condition: Condition("[BODY].data == 0o2"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0o2\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 0o2",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-binary-int-using-greater-than",
|
||||
Condition: Condition("[BODY].data > 0"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0b1\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data > 0",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-binary-int-using-equal",
|
||||
Condition: Condition("[BODY].data == 2"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0b0010\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 2",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-binary-int-using-equals",
|
||||
Condition: Condition("[BODY].data == 0b10"),
|
||||
Result: &Result{Body: []byte("{\"data\": \"0b0010\"}")},
|
||||
ExpectedSuccess: true,
|
||||
ExpectedOutput: "[BODY].data == 0b10",
|
||||
},
|
||||
{
|
||||
Name: "body-jsonpath-complex-int-using-greater-than-failure",
|
||||
Condition: Condition("[BODY].data.id > 5"),
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/client"
|
||||
"github.com/TwiN/gatus/v5/core/ui"
|
||||
"github.com/TwiN/gatus/v5/util"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type EndpointType string
|
||||
@@ -42,6 +43,8 @@ const (
|
||||
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
|
||||
EndpointTypeTLS EndpointType = "TLS"
|
||||
EndpointTypeHTTP EndpointType = "HTTP"
|
||||
EndpointTypeWS EndpointType = "WEBSOCKET"
|
||||
EndpointTypeSSH EndpointType = "SSH"
|
||||
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
|
||||
)
|
||||
|
||||
@@ -69,6 +72,10 @@ var (
|
||||
// This is because the free whois service we are using should not be abused, especially considering the fact that
|
||||
// the data takes a while to be updated.
|
||||
ErrInvalidEndpointIntervalForDomainExpirationPlaceholder = errors.New("the minimum interval for an endpoint with a condition using the " + DomainExpirationPlaceholder + " placeholder is 300s (5m)")
|
||||
// ErrEndpointWithoutSSHUsername is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a user.
|
||||
ErrEndpointWithoutSSHUsername = errors.New("you must specify a username for each endpoint with SSH")
|
||||
// ErrEndpointWithoutSSHPassword is the error with which Gatus will panic if an endpoint with SSH monitoring is configured without a password.
|
||||
ErrEndpointWithoutSSHPassword = errors.New("you must specify a password for each endpoint with SSH")
|
||||
)
|
||||
|
||||
// Endpoint is the configuration of a monitored
|
||||
@@ -120,6 +127,27 @@ type Endpoint struct {
|
||||
|
||||
// NumberOfSuccessesInARow is the number of successful evaluations in a row
|
||||
NumberOfSuccessesInARow int `yaml:"-"`
|
||||
|
||||
// SSH is the configuration of SSH monitoring.
|
||||
SSH *SSH `yaml:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
type SSH struct {
|
||||
// Username is the username to use when connecting to the SSH server.
|
||||
Username string `yaml:"username,omitempty"`
|
||||
// Password is the password to use when connecting to the SSH server.
|
||||
Password string `yaml:"password,omitempty"`
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the endpoint
|
||||
func (s *SSH) ValidateAndSetDefaults() error {
|
||||
if s.Username == "" {
|
||||
return ErrEndpointWithoutSSHUsername
|
||||
}
|
||||
if s.Password == "" {
|
||||
return ErrEndpointWithoutSSHPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the endpoint is enabled or not
|
||||
@@ -149,6 +177,10 @@ func (endpoint Endpoint) Type() EndpointType {
|
||||
return EndpointTypeTLS
|
||||
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
|
||||
return EndpointTypeHTTP
|
||||
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
|
||||
return EndpointTypeWS
|
||||
case strings.HasPrefix(endpoint.URL, "ssh://"):
|
||||
return EndpointTypeSSH
|
||||
default:
|
||||
return EndpointTypeUNKNOWN
|
||||
}
|
||||
@@ -225,6 +257,9 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if endpoint.SSH != nil {
|
||||
return endpoint.SSH.ValidateAndSetDefaults()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -340,6 +375,26 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeICMP {
|
||||
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
|
||||
} else if endpointType == EndpointTypeWS {
|
||||
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.Body, endpoint.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
} else if endpointType == EndpointTypeSSH {
|
||||
var cli *ssh.Client
|
||||
result.Connected, cli, err = client.CanCreateSSHConnection(strings.TrimPrefix(endpoint.URL, "ssh://"), endpoint.SSH.Username, endpoint.SSH.Password, endpoint.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Success, result.HTTPStatus, err = client.ExecuteSSHCommand(cli, endpoint.Body, endpoint.ClientConfig)
|
||||
if err != nil {
|
||||
result.AddError(err.Error())
|
||||
return
|
||||
}
|
||||
result.Duration = time.Since(startTime)
|
||||
} else {
|
||||
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
|
||||
result.Duration = time.Since(startTime)
|
||||
@@ -364,6 +419,15 @@ func (endpoint *Endpoint) call(result *Result) {
|
||||
}
|
||||
}
|
||||
|
||||
// Close HTTP connections between watchdog and endpoints to avoid dangling socket file descriptors
|
||||
// on configuration reload.
|
||||
// More context on https://github.com/TwiN/gatus/issues/536
|
||||
func (endpoint *Endpoint) Close() {
|
||||
if endpoint.Type() == EndpointTypeHTTP {
|
||||
client.GetHTTPClient(endpoint.ClientConfig).CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
func (endpoint *Endpoint) buildHTTPRequest() *http.Request {
|
||||
var bodyBuffer *bytes.Buffer
|
||||
if endpoint.GraphQL {
|
||||
|
||||
@@ -256,6 +256,7 @@ func TestEndpoint_Type(t *testing.T) {
|
||||
type args struct {
|
||||
URL string
|
||||
DNS *DNS
|
||||
SSH *SSH
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
@@ -313,6 +314,28 @@ func TestEndpoint_Type(t *testing.T) {
|
||||
},
|
||||
want: EndpointTypeHTTP,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "wss://example.com/",
|
||||
},
|
||||
want: EndpointTypeWS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "ws://example.com/",
|
||||
},
|
||||
want: EndpointTypeWS,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "ssh://example.com:22",
|
||||
SSH: &SSH{
|
||||
Username: "root",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
want: EndpointTypeSSH,
|
||||
},
|
||||
{
|
||||
args: args{
|
||||
URL: "invalid://example.org",
|
||||
@@ -442,6 +465,52 @@ func TestEndpoint_ValidateAndSetDefaultsWithDNS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaultsWithSSH(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "fail when has no user",
|
||||
username: "",
|
||||
password: "password",
|
||||
expectedErr: ErrEndpointWithoutSSHUsername,
|
||||
},
|
||||
{
|
||||
name: "fail when has no password",
|
||||
username: "username",
|
||||
password: "",
|
||||
expectedErr: ErrEndpointWithoutSSHPassword,
|
||||
},
|
||||
{
|
||||
name: "success when all fields are set",
|
||||
username: "username",
|
||||
password: "password",
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
endpoint := &Endpoint{
|
||||
Name: "ssh-test",
|
||||
URL: "https://example.com",
|
||||
SSH: &SSH{
|
||||
Username: test.username,
|
||||
Password: test.password,
|
||||
},
|
||||
Conditions: []Condition{Condition("[STATUS] == 0")},
|
||||
}
|
||||
err := endpoint.ValidateAndSetDefaults()
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("expected error %v, got %v", test.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndpoint_ValidateAndSetDefaultsWithSimpleErrors(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
endpoint *Endpoint
|
||||
@@ -668,6 +737,55 @@ func TestIntegrationEvaluateHealthForDNS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForSSH(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint Endpoint
|
||||
conditions []Condition
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
name: "ssh-success",
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-success",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
conditions: []Condition{Condition("[STATUS] == 0")},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
name: "ssh-failure",
|
||||
endpoint: Endpoint{
|
||||
Name: "ssh-failure",
|
||||
URL: "ssh://localhost",
|
||||
SSH: &SSH{
|
||||
Username: "test",
|
||||
Password: "test",
|
||||
},
|
||||
Body: "{ \"command\": \"uptime\" }",
|
||||
},
|
||||
conditions: []Condition{Condition("[STATUS] == 1")},
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.endpoint.ValidateAndSetDefaults()
|
||||
test.endpoint.Conditions = test.conditions
|
||||
result := test.endpoint.EvaluateHealth()
|
||||
if result.Success != test.success {
|
||||
t.Errorf("Expected success to be %v, but was %v", test.success, result.Success)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationEvaluateHealthForICMP(t *testing.T) {
|
||||
endpoint := Endpoint{
|
||||
Name: "icmp-test",
|
||||
|
||||
93
go.mod
93
go.mod
@@ -1,61 +1,84 @@
|
||||
module github.com/TwiN/gatus/v5
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/TwiN/deepmerge v0.2.0
|
||||
github.com/TwiN/g8 v1.4.0
|
||||
github.com/TwiN/deepmerge v0.2.1
|
||||
github.com/TwiN/g8/v2 v2.0.0
|
||||
github.com/TwiN/gocache/v2 v2.2.0
|
||||
github.com/TwiN/health v1.6.0
|
||||
github.com/TwiN/whois v1.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.5.0
|
||||
github.com/TwiN/whois v1.1.7
|
||||
github.com/aws/aws-sdk-go v1.47.9
|
||||
github.com/coreos/go-oidc/v3 v3.7.0
|
||||
github.com/gofiber/fiber/v2 v2.52.1
|
||||
github.com/google/go-github/v48 v48.2.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/miekg/dns v1.1.53
|
||||
github.com/prometheus-community/pro-bing v0.1.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/oauth2 v0.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/miekg/dns v1.1.56
|
||||
github.com/prometheus-community/pro-bing v0.3.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.1
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/net v0.22.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
google.golang.org/api v0.148.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.20.4
|
||||
modernc.org/sqlite v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/blend/go-sdk v1.20220411.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/sync v0.4.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
|
||||
google.golang.org/grpc v1.58.3 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.2 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.4.0 // indirect
|
||||
modernc.org/libc v1.29.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.7.2 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
|
||||
606
go.sum
606
go.sum
@@ -1,533 +1,255 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/TwiN/deepmerge v0.2.0 h1:8P1tp2qDXllX6V1Ailg2XA074easAcvLMmW9v1jn0aE=
|
||||
github.com/TwiN/deepmerge v0.2.0/go.mod h1:cR9OWsvI13y+FxrbnPLuF6BX2tbYeOjkiI6JWjEGqAE=
|
||||
github.com/TwiN/g8 v1.4.0 h1:RUk5xTtxKCdMo0GGSbBVyjtAAfi2nqVbA9E0C4u5Cxo=
|
||||
github.com/TwiN/g8 v1.4.0/go.mod h1:ECyGJsoIb99klUfvVQoS1StgRLte9yvvPigGrHdy284=
|
||||
github.com/TwiN/deepmerge v0.2.1 h1:GowJr9O4THTVW4awX63x1BVg1hgr4q+35XKKCYbwsSs=
|
||||
github.com/TwiN/deepmerge v0.2.1/go.mod h1:LVBmCEBQvibYSF8Gyl/NqhHXH7yIiT7Ozqf9dHxGPW0=
|
||||
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
|
||||
github.com/TwiN/g8/v2 v2.0.0/go.mod h1:4sVAF27q8T8ISggRa/Fb0drw7wpB22B6eWd+/+SGMqE=
|
||||
github.com/TwiN/gocache/v2 v2.2.0 h1:M3B36KyH24BntxLrLaUb2kgTdq8DzCnfod0IekLG57w=
|
||||
github.com/TwiN/gocache/v2 v2.2.0/go.mod h1:SnUuBsrwGQeNcDG6vhkOMJnqErZM0JGjgIkuKryokYA=
|
||||
github.com/TwiN/health v1.6.0 h1:L2ks575JhRgQqWWOfKjw9B0ec172hx7GdToqkYUycQM=
|
||||
github.com/TwiN/health v1.6.0/go.mod h1:Z6TszwQPMvtSiVx1QMidVRgvVr4KZGfiwqcD7/Z+3iw=
|
||||
github.com/TwiN/whois v1.1.0 h1:lhyrC/9yIXntEnbJ+0IBy9Z5NBcreieYyamlvniwq88=
|
||||
github.com/TwiN/whois v1.1.0/go.mod h1:9WbCzYlR+r5eq9vbgJVh7A4H2uR2ct4wKEB0/QITJ/c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/TwiN/whois v1.1.7 h1:eGzLOrWhpYLAGXD8boXh0bBKllN/EmuBsLqTJT4tC/U=
|
||||
github.com/TwiN/whois v1.1.7/go.mod h1:VOJAH4+3chAik5gva5zxJNXv2voEHjMNCf1y07sqj9w=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/aws/aws-sdk-go v1.47.9 h1:rarTsos0mA16q+huicGx0e560aYRtOucV5z2Mw23JRY=
|
||||
github.com/aws/aws-sdk-go v1.47.9/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
|
||||
github.com/blend/go-sdk v1.20220411.3/go.mod h1:7lnH8fTi6U4i1fArEXRyOIY2E1X4MALg09qsQqY1+ak=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
|
||||
github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
|
||||
github.com/coreos/go-oidc/v3 v3.7.0 h1:FTdj0uexT4diYIPlF4yoFVI5MRO1r5+SEcIpEw9vC0o=
|
||||
github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
|
||||
github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE=
|
||||
github.com/google/go-github/v48 v48.2.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g=
|
||||
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus-community/pro-bing v0.1.0 h1:zjzLGhfNPP0bP1OlzGB+SJcguOViw7df12LPg2vUJh8=
|
||||
github.com/prometheus-community/pro-bing v0.1.0/go.mod h1:BpWlHurD9flHtzq8wrh8QGWYz9ka9z9ZJAyOel8ej58=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
|
||||
github.com/prometheus-community/pro-bing v0.3.0/go.mod h1:p9dLb9zdmv+eLxWfCT6jESWuDrS+YzpPkQBgysQF8a0=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
|
||||
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs=
|
||||
google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
|
||||
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -536,39 +258,26 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
@@ -576,23 +285,24 @@ modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
|
||||
modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
|
||||
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
|
||||
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
|
||||
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
|
||||
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
|
||||
14
main.go
14
main.go
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +15,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if delayInSeconds, _ := strconv.Atoi(os.Getenv("GATUS_DELAY_START_SECONDS")); delayInSeconds > 0 {
|
||||
log.Printf("Delaying start by %d seconds", delayInSeconds)
|
||||
time.Sleep(time.Duration(delayInSeconds) * time.Second)
|
||||
}
|
||||
cfg, err := loadConfiguration()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -27,7 +32,7 @@ func main() {
|
||||
go func() {
|
||||
<-signalChannel
|
||||
log.Println("Received termination signal, attempting to gracefully shut down")
|
||||
stop()
|
||||
stop(cfg)
|
||||
save()
|
||||
done <- true
|
||||
}()
|
||||
@@ -41,8 +46,8 @@ func start(cfg *config.Config) {
|
||||
go listenToConfigurationFileChanges(cfg)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
watchdog.Shutdown()
|
||||
func stop(cfg *config.Config) {
|
||||
watchdog.Shutdown(cfg)
|
||||
controller.Shutdown()
|
||||
}
|
||||
|
||||
@@ -89,7 +94,7 @@ func listenToConfigurationFileChanges(cfg *config.Config) {
|
||||
time.Sleep(30 * time.Second)
|
||||
if cfg.HasLoadedConfigurationBeenModified() {
|
||||
log.Println("[main][listenToConfigurationFileChanges] Configuration file has been modified")
|
||||
stop()
|
||||
stop(cfg)
|
||||
time.Sleep(time.Second) // Wait a bit to make sure everything is done.
|
||||
save()
|
||||
updatedConfig, err := loadConfiguration()
|
||||
@@ -104,6 +109,7 @@ func listenToConfigurationFileChanges(cfg *config.Config) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
store.Get().Close()
|
||||
initializeStorage(updatedConfig)
|
||||
start(updatedConfig)
|
||||
return
|
||||
|
||||
@@ -2,10 +2,13 @@ package security
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/TwiN/g8"
|
||||
"github.com/gorilla/mux"
|
||||
g8 "github.com/TwiN/g8/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||
"github.com/gofiber/fiber/v2/middleware/basicauth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -29,20 +32,20 @@ func (c *Config) IsValid() bool {
|
||||
}
|
||||
|
||||
// RegisterHandlers registers all handlers required based on the security configuration
|
||||
func (c *Config) RegisterHandlers(router *mux.Router) error {
|
||||
func (c *Config) RegisterHandlers(router fiber.Router) error {
|
||||
if c.OIDC != nil {
|
||||
if err := c.OIDC.initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
router.HandleFunc("/oidc/login", c.OIDC.loginHandler)
|
||||
router.HandleFunc("/authorization-code/callback", c.OIDC.callbackHandler)
|
||||
router.All("/oidc/login", c.OIDC.loginHandler)
|
||||
router.All("/authorization-code/callback", adaptor.HTTPHandlerFunc(c.OIDC.callbackHandler))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplySecurityMiddleware applies an authentication middleware to the router passed.
|
||||
// The router passed should be a subrouter in charge of handlers that require authentication.
|
||||
func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
|
||||
// The router passed should be a sub-router in charge of handlers that require authentication.
|
||||
func (c *Config) ApplySecurityMiddleware(router fiber.Router) error {
|
||||
if c.OIDC != nil {
|
||||
// We're going to use g8 for session handling
|
||||
clientProvider := g8.NewClientProvider(func(token string) *g8.Client {
|
||||
@@ -61,7 +64,7 @@ func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
|
||||
// TODO: g8: Add a way to update cookie after? would need the writer
|
||||
authorizationService := g8.NewAuthorizationService().WithClientProvider(clientProvider)
|
||||
c.gate = g8.New().WithAuthorizationService(authorizationService).WithCustomTokenExtractor(customTokenExtractorFunc)
|
||||
api.Use(c.gate.Protect)
|
||||
router.Use(adaptor.HTTPMiddleware(c.gate.Protect))
|
||||
} else if c.Basic != nil {
|
||||
var decodedBcryptHash []byte
|
||||
if len(c.Basic.PasswordBcryptHashBase64Encoded) > 0 {
|
||||
@@ -71,29 +74,35 @@ func (c *Config) ApplySecurityMiddleware(api *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
api.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
usernameEntered, passwordEntered, ok := r.BasicAuth()
|
||||
router.Use(basicauth.New(basicauth.Config{
|
||||
Authorizer: func(username, password string) bool {
|
||||
if len(c.Basic.PasswordBcryptHashBase64Encoded) > 0 {
|
||||
if !ok || usernameEntered != c.Basic.Username || bcrypt.CompareHashAndPassword(decodedBcryptHash, []byte(passwordEntered)) != nil {
|
||||
w.Header().Set("WWW-Authenticate", "Basic")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
if username != c.Basic.Username || bcrypt.CompareHashAndPassword(decodedBcryptHash, []byte(password)) != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
return true
|
||||
},
|
||||
Unauthorized: func(ctx *fiber.Ctx) error {
|
||||
ctx.Set("WWW-Authenticate", "Basic")
|
||||
return ctx.Status(401).SendString("Unauthorized")
|
||||
},
|
||||
}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsAuthenticated checks whether the user is authenticated
|
||||
// If the Config does not warrant authentication, it will always return true.
|
||||
func (c *Config) IsAuthenticated(r *http.Request) bool {
|
||||
func (c *Config) IsAuthenticated(ctx *fiber.Ctx) bool {
|
||||
if c.gate != nil {
|
||||
token := c.gate.ExtractTokenFromRequest(r)
|
||||
// TODO: Update g8 to support fasthttp natively? (see g8's fasthttp branch)
|
||||
request, err := adaptor.ConvertRequest(ctx, false)
|
||||
if err != nil {
|
||||
log.Printf("[IsAuthenticated] Unexpected error converting request: %v", err)
|
||||
return false
|
||||
}
|
||||
token := c.gate.ExtractTokenFromRequest(request)
|
||||
_, hasSession := sessions.Get(token)
|
||||
return hasSession
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -23,83 +23,96 @@ func TestConfig_ApplySecurityMiddleware(t *testing.T) {
|
||||
///////////
|
||||
// BASIC //
|
||||
///////////
|
||||
// Bcrypt
|
||||
c := &Config{Basic: &BasicConfig{
|
||||
Username: "john.doe",
|
||||
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
|
||||
}}
|
||||
api := mux.NewRouter()
|
||||
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
// Bcrypt
|
||||
c := &Config{Basic: &BasicConfig{
|
||||
Username: "john.doe",
|
||||
PasswordBcryptHashBase64Encoded: "JDJhJDA4JDFoRnpPY1hnaFl1OC9ISlFsa21VS09wOGlPU1ZOTDlHZG1qeTFvb3dIckRBUnlHUmNIRWlT",
|
||||
}}
|
||||
app := fiber.New()
|
||||
if err := c.ApplySecurityMiddleware(app); err != nil {
|
||||
t.Error("expected no error, got", err)
|
||||
}
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(200)
|
||||
})
|
||||
// Try to access the route without basic auth
|
||||
request := httptest.NewRequest("GET", "/test", http.NoBody)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 401 {
|
||||
t.Error("expected code to be 401, but was", response.StatusCode)
|
||||
}
|
||||
// Try again, but with basic auth
|
||||
request = httptest.NewRequest("GET", "/test", http.NoBody)
|
||||
request.SetBasicAuth("john.doe", "hunter2")
|
||||
response, err = app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 200 {
|
||||
t.Error("expected code to be 200, but was", response.StatusCode)
|
||||
}
|
||||
})
|
||||
if err := c.ApplySecurityMiddleware(api); err != nil {
|
||||
t.Error("expected no error, but was", err)
|
||||
}
|
||||
// Try to access the route without basic auth
|
||||
request, _ := http.NewRequest("GET", "/test", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusUnauthorized {
|
||||
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||
}
|
||||
// Try again, but with basic auth
|
||||
request, _ = http.NewRequest("GET", "/test", http.NoBody)
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
request.SetBasicAuth("john.doe", "hunter2")
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusOK {
|
||||
t.Error("expected code to be 200, but was", responseRecorder.Code)
|
||||
}
|
||||
//////////
|
||||
// OIDC //
|
||||
//////////
|
||||
api = mux.NewRouter()
|
||||
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
t.Run("oidc", func(t *testing.T) {
|
||||
c := &Config{OIDC: &OIDCConfig{
|
||||
IssuerURL: "https://sso.gatus.io/",
|
||||
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||
Scopes: []string{"openid"},
|
||||
AllowedSubjects: []string{"user1@example.com"},
|
||||
oauth2Config: oauth2.Config{},
|
||||
verifier: nil,
|
||||
}}
|
||||
app := fiber.New()
|
||||
if err := c.ApplySecurityMiddleware(app); err != nil {
|
||||
t.Error("expected no error, got", err)
|
||||
}
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(200)
|
||||
})
|
||||
// Try without any session cookie
|
||||
request := httptest.NewRequest("GET", "/test", http.NoBody)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 401 {
|
||||
t.Error("expected code to be 401, but was", response.StatusCode)
|
||||
}
|
||||
// Try with a session cookie
|
||||
request = httptest.NewRequest("GET", "/test", http.NoBody)
|
||||
request.AddCookie(&http.Cookie{Name: "session", Value: "123"})
|
||||
response, err = app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 401 {
|
||||
t.Error("expected code to be 401, but was", response.StatusCode)
|
||||
}
|
||||
})
|
||||
c.OIDC = &OIDCConfig{
|
||||
IssuerURL: "https://sso.gatus.io/",
|
||||
RedirectURL: "http://localhost:80/authorization-code/callback",
|
||||
Scopes: []string{"openid"},
|
||||
AllowedSubjects: []string{"user1@example.com"},
|
||||
oauth2Config: oauth2.Config{},
|
||||
verifier: nil,
|
||||
}
|
||||
c.Basic = nil
|
||||
if err := c.ApplySecurityMiddleware(api); err != nil {
|
||||
t.Error("expected no error, but was", err)
|
||||
}
|
||||
// Try without any session cookie
|
||||
request, _ = http.NewRequest("GET", "/test", http.NoBody)
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusUnauthorized {
|
||||
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||
}
|
||||
// Try with a session cookie
|
||||
request, _ = http.NewRequest("GET", "/test", http.NoBody)
|
||||
request.AddCookie(&http.Cookie{Name: "session", Value: "123"})
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
api.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusUnauthorized {
|
||||
t.Error("expected code to be 401, but was", responseRecorder.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_RegisterHandlers(t *testing.T) {
|
||||
c := &Config{}
|
||||
router := mux.NewRouter()
|
||||
c.RegisterHandlers(router)
|
||||
app := fiber.New()
|
||||
c.RegisterHandlers(app)
|
||||
// Try to access the OIDC handler. This should fail, because the security config doesn't have OIDC
|
||||
request, _ := http.NewRequest("GET", "/oidc/login", http.NoBody)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusNotFound {
|
||||
t.Error("expected code to be 404, but was", responseRecorder.Code)
|
||||
request := httptest.NewRequest("GET", "/oidc/login", http.NoBody)
|
||||
response, err := app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 404 {
|
||||
t.Error("expected code to be 404, but was", response.StatusCode)
|
||||
}
|
||||
// Set an empty OIDC config. This should fail, because the IssuerURL is required.
|
||||
c.OIDC = &OIDCConfig{}
|
||||
if err := c.RegisterHandlers(router); err == nil {
|
||||
if err := c.RegisterHandlers(app); err == nil {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
// Set the OIDC config and try again
|
||||
@@ -109,13 +122,15 @@ func TestConfig_RegisterHandlers(t *testing.T) {
|
||||
Scopes: []string{"openid"},
|
||||
AllowedSubjects: []string{"user1@example.com"},
|
||||
}
|
||||
if err := c.RegisterHandlers(router); err != nil {
|
||||
if err := c.RegisterHandlers(app); err != nil {
|
||||
t.Fatal("expected no error, but got", err)
|
||||
}
|
||||
request, _ = http.NewRequest("GET", "/oidc/login", http.NoBody)
|
||||
responseRecorder = httptest.NewRecorder()
|
||||
router.ServeHTTP(responseRecorder, request)
|
||||
if responseRecorder.Code != http.StatusFound {
|
||||
t.Error("expected code to be 302, but was", responseRecorder.Code)
|
||||
request = httptest.NewRequest("GET", "/oidc/login", http.NoBody)
|
||||
response, err = app.Test(request)
|
||||
if err != nil {
|
||||
t.Fatal("expected no error, got", err)
|
||||
}
|
||||
if response.StatusCode != 302 {
|
||||
t.Error("expected code to be 302, but was", response.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user