mirror of
https://github.com/TwiN/gatus.git
synced 2026-08-25 00:26:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08c463542f | ||
|
|
0fd2520088 | ||
|
|
7a73be52c5 | ||
|
|
73381e4179 | ||
|
|
b7100620c3 | ||
|
|
ae7ca199aa | ||
|
|
53392dd472 | ||
|
|
4566e746f3 | ||
|
|
08895200bb | ||
|
|
809e5068d7 | ||
|
|
ed1107b41a | ||
|
|
154ee6e055 | ||
|
|
9095a5254e | ||
|
|
fba833f64b | ||
|
|
834bfab5c5 | ||
|
|
f9e03cb2c3 | ||
|
|
760b75348e | ||
|
|
447c7ad321 | ||
|
|
bc433161d6 | ||
|
|
42b51f5da5 | ||
|
|
48056def12 | ||
|
|
f06c4f132b | ||
|
|
64116ce3da | ||
|
|
12be9facda | ||
|
|
f925cb4924 | ||
|
|
f4428aa600 | ||
|
|
0e390765e3 | ||
|
|
893601a5f4 | ||
|
|
322f278e8f | ||
|
|
814a9622c4 | ||
|
|
a2fc5ab9bb | ||
|
|
593826ec5f | ||
|
|
9b2efa290f | ||
|
|
714143204b | ||
|
|
ff396b5079 | ||
|
|
b20935ecfe | ||
|
|
3a6ad14cba | ||
|
|
e6e211c916 | ||
|
|
7cd09e7d03 | ||
|
|
fa8a387c9f | ||
|
|
c7f0b85ba1 | ||
|
|
c4900ced6c | ||
|
|
433e59b0ce | ||
|
|
f09d959c97 | ||
|
|
e5b4e0d381 | ||
|
|
e580cd362a | ||
|
|
b65b4d83e1 | ||
|
|
618892c248 | ||
|
|
77cedb080d | ||
|
|
786af2dee4 | ||
|
|
48531911bb | ||
|
|
720888009e | ||
|
|
961bffd8e7 | ||
|
|
309d68e622 | ||
|
|
ce269905b7 | ||
|
|
d8724d683e | ||
|
|
0f22e863a9 | ||
|
|
7bb959e072 | ||
|
|
1095deb3c6 | ||
|
|
c93a321073 | ||
|
|
1bf05d6bf9 |
@@ -2,7 +2,9 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/db:/var/lib/postgresql/data
|
# PostgreSQL 18 and above declare the volume at /var/lib/postgresql.
|
||||||
|
# For 17 and earlier, mount ./data/db at /var/lib/postgresql/data instead.
|
||||||
|
- ./data/db:/var/lib/postgresql
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
ignore:
|
|
||||||
- "storage/store/sql/specific_postgres.go" # Can't test for postgres
|
|
||||||
- "watchdog/endpoint.go"
|
|
||||||
- "watchdog/external_endpoint.go"
|
|
||||||
- "watchdog/suite.go"
|
|
||||||
- "watchdog/watchdog.go"
|
|
||||||
comment: false
|
|
||||||
coverage:
|
|
||||||
status:
|
|
||||||
patch: off
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: 70%
|
|
||||||
threshold: null
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See AGENTS.md for agent instructions
|
||||||
@@ -22,9 +22,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.4
|
go-version: 1.26.3
|
||||||
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
repository: "${{ github.event.inputs.repository || 'TwiN/gatus' }}"
|
||||||
ref: "${{ github.event.inputs.ref || 'master' }}"
|
ref: "${{ github.event.inputs.ref || 'master' }}"
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Benchmark
|
- name: Benchmark
|
||||||
run: go test -bench=. ./storage/store
|
run: go test -bench=. ./storage/store
|
||||||
|
|||||||
@@ -19,28 +19,28 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
run: echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ${{ env.GHCR_IMAGE_REPOSITORY }}
|
images: ${{ env.GHCR_IMAGE_REPOSITORY }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ inputs.tag }}
|
type=raw,value=${{ inputs.tag }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
platforms: ${{ inputs.platforms }}
|
platforms: ${{ inputs.platforms }}
|
||||||
pull: true
|
pull: true
|
||||||
|
|||||||
@@ -5,27 +5,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
run: echo IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ${{ env.IMAGE_REPOSITORY }}
|
images: ${{ env.IMAGE_REPOSITORY }}
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=experimental
|
type=raw,value=experimental
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
pull: true
|
pull: true
|
||||||
|
|||||||
@@ -13,29 +13,29 @@ jobs:
|
|||||||
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
|
if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}
|
||||||
timeout-minutes: 240
|
timeout-minutes: 240
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: |
|
run: |
|
||||||
echo DOCKER_IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
echo DOCKER_IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
echo GHCR_IMAGE_REPOSITORY=$(echo ghcr.io/${{ github.actor }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_IMAGE_REPOSITORY }}
|
${{ env.DOCKER_IMAGE_REPOSITORY }}
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 240
|
timeout-minutes: 240
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Get image repository
|
- name: Get image repository
|
||||||
run: |
|
run: |
|
||||||
echo DOCKER_IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
echo DOCKER_IMAGE_REPOSITORY=$(echo ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
@@ -20,19 +20,19 @@ jobs:
|
|||||||
- name: Get the release
|
- name: Get the release
|
||||||
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
|
run: echo RELEASE=${GITHUB_REF/refs\/tags\//} >> $GITHUB_ENV
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_IMAGE_REPOSITORY }}
|
${{ env.DOCKER_IMAGE_REPOSITORY }}
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
type=raw,value=stable
|
type=raw,value=stable
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
pull: true
|
pull: true
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Get PR branch
|
- name: Get PR branch
|
||||||
id: pr
|
id: pr
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = await github.rest.pulls.get({
|
const pr = await github.rest.pulls.get({
|
||||||
@@ -46,13 +46,13 @@ jobs:
|
|||||||
core.setOutput('ref', pr.data.head.ref);
|
core.setOutput('ref', pr.data.head.ref);
|
||||||
core.setOutput('repo', pr.data.head.repo.full_name);
|
core.setOutput('repo', pr.data.head.repo.full_name);
|
||||||
- name: Checkout PR branch
|
- name: Checkout PR branch
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
repository: ${{ steps.pr.outputs.repo }}
|
repository: ${{ steps.pr.outputs.repo }}
|
||||||
ref: ${{ steps.pr.outputs.ref }}
|
ref: ${{ steps.pr.outputs.ref }}
|
||||||
- name: Regenerate static assets
|
- name: Regenerate static assets
|
||||||
run: |
|
run: |
|
||||||
make frontend-install-dependencies
|
make frontend-install
|
||||||
make frontend-build
|
make frontend-build
|
||||||
- name: Commit and push changes
|
- name: Commit and push changes
|
||||||
id: commit
|
id: commit
|
||||||
@@ -79,24 +79,27 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create response comment
|
- name: Create response comment
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const status = '${{ needs.regenerate-static-assets.outputs.status }}';
|
const status = '${{ needs.regenerate-static-assets.outputs.status }}';
|
||||||
let reaction = '';
|
let reaction = 'hooray';
|
||||||
if (status === 'success') {
|
let message = '';
|
||||||
reaction = 'hooray';
|
var workflowUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
|
||||||
} else if (status === 'no_changes') {
|
if (status === 'no_changes') {
|
||||||
reaction = '+1';
|
message = `@${context.actor} No changes to commit ([ref](${workflowUrl})).`;
|
||||||
|
} else if (status === 'success') {
|
||||||
|
// Assets regenerated successfully - no comment needed, just add reaction
|
||||||
} else {
|
} else {
|
||||||
reaction = '-1';
|
reaction = '-1';
|
||||||
var workflowUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
|
message = `@${context.actor} There was an issue regenerating static assets. Please check the [workflow run logs](${workflowUrl}) for more details.`;
|
||||||
var body = '⚠️ There was an issue regenerating static assets. Please check the [workflow run logs](' + workflowUrl + ') for more details.';
|
}
|
||||||
|
if (message.length) {
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
body: body
|
body: message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await github.rest.reactions.createForIssueComment({
|
await github.rest.reactions.createForIssueComment({
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- run: make frontend-install-dependencies
|
- run: make frontend-install
|
||||||
- run: make frontend-build
|
- run: make frontend-build
|
||||||
@@ -18,17 +18,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.4
|
go-version: 1.26.3
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7
|
||||||
- name: Build binary to make sure it works
|
- name: Build binary to make sure it works
|
||||||
run: go build
|
run: go build
|
||||||
- name: Test
|
- name: Test
|
||||||
# We're using "sudo" because one of the tests leverages ping, which requires super-user privileges.
|
# We're using "sudo" because one of the tests leverages ping, which requires super-user privileges.
|
||||||
# As for the 'env "PATH=$PATH" "GOROOT=$GOROOT"', we need it to use the same "go" executable that
|
# As for the 'env "PATH=$PATH" "GOROOT=$GOROOT"', we need it to use the same "go" executable that
|
||||||
# was configured by the "Set up Go" step (otherwise, it'd use sudo's "go" executable)
|
# 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
|
run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test ./... -race
|
||||||
- name: Codecov
|
|
||||||
uses: codecov/codecov-action@v5.5.2
|
|
||||||
with:
|
|
||||||
files: ./coverage.txt
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for AI coding agents working in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `make install` — Build the `gatus` binary
|
||||||
|
- `make run` — Run in dev mode (uses `./config.yaml`)
|
||||||
|
- `make test` — Run all Go tests with coverage
|
||||||
|
- `make clean` — Remove built binary
|
||||||
|
- `make frontend-install` — Install frontend npm dependencies
|
||||||
|
- `make frontend-build` — Build the Vue.js frontend (**must run after any `web/` changes**)
|
||||||
|
- `make frontend-dev` — Start the Vue dev server
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
- **Vendored dependencies**: After adding/updating a Go dependency, run `go mod tidy && go mod vendor`
|
||||||
|
- **Frontend build artifacts**: `web/static/` is generated by `make frontend-build` and embedded into the Go binary at compile time via `//go:embed`. Never edit files in `web/static/` directly
|
||||||
|
- **Config validation order**: In `config/config.go` `parseAndValidateConfigBytes`, `ValidateAlertingConfig` **must** run before `ValidateEndpointsConfig` because provider default alerts must be parsed before endpoint defaults are set
|
||||||
|
- **Invalid configs panic at startup** — validation errors are fatal, not graceful
|
||||||
|
- **Hot-reload**: `main.go` watches the config file and re-runs the full stop → save → load → init → start cycle. New startup logic must account for this
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `GATUS_CONFIG_PATH` — Config file path (default: `config/config.yaml`); can be a directory of YAML files (deep-merged)
|
||||||
|
- `GATUS_LOG_LEVEL` — `DEBUG`, `INFO`, `WARN`, `ERROR`
|
||||||
|
- `ENVIRONMENT=dev` — Enables CORS for frontend dev on `localhost:8081`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `main.go` — Entry point; loads config, initializes storage, starts watchdog + API server
|
||||||
|
- `config/` — Central config struct (`config.go`) and sub-packages per config section (each with `ValidateAndSetDefaults()`)
|
||||||
|
- `alerting/` — Alerting config (`config.go`), alert types (`alert/type.go`), and one sub-package per provider under `provider/`
|
||||||
|
- `api/` — Fiber-based HTTP API routes and SPA serving
|
||||||
|
- `watchdog/` — Monitoring loop; spawns a goroutine per endpoint with semaphore-based concurrency control
|
||||||
|
- `client/` — HTTP/gRPC client used for health checks
|
||||||
|
- `storage/store/` — Storage abstraction with `memory/`, `sql/` (SQLite + PostgreSQL) implementations
|
||||||
|
- `security/` — Authentication (basic + OIDC) and security middleware
|
||||||
|
- `web/app/` — Vue 3 frontend source; builds to `web/static/` (embedded into the Go binary)
|
||||||
|
- `vendor/` — Vendored Go dependencies (committed to repo)
|
||||||
|
|
||||||
|
## Common Changes
|
||||||
|
|
||||||
|
### Adding an Alerting Provider
|
||||||
|
|
||||||
|
Use `alerting/provider/slack/` as the reference implementation. Every new provider touches **6 locations**:
|
||||||
|
|
||||||
|
1. **Create provider package** — `alerting/provider/{name}/{name}.go` + `{name}_test.go`
|
||||||
|
- `Config` struct with `Validate()` and `Merge(*Config)` methods
|
||||||
|
- `AlertProvider` struct with `DefaultConfig` (yaml inline), `DefaultAlert *alert.Alert`, `Overrides []Override`
|
||||||
|
- Use `client.GetHTTPClient(nil)` (not `http.DefaultClient`), always `defer response.Body.Close()`
|
||||||
|
2. **Register type** — Add `Type{Name} Type = "{name}"` in `alerting/alert/type.go`
|
||||||
|
3. **Add to config struct** — Add field to `alerting.Config` in `alerting/config.go`; the **YAML tag must exactly match** the `alert.Type` string (reflection-based lookup via `GetAlertingProviderByAlertType`)
|
||||||
|
4. **Add compile-time checks** — Both `_ AlertProvider = (*pkg.AlertProvider)(nil)` **and** `_ Config[pkg.Config] = (*pkg.Config)(nil)` in `alerting/provider/provider.go`
|
||||||
|
5. **Register in validation** — Add the type to the `alertTypes` slice in `config/config.go` `ValidateAlertingConfig`
|
||||||
|
6. **Document** — Add to README.md in alphabetical order
|
||||||
|
|
||||||
|
### Adding a Config Section
|
||||||
|
|
||||||
|
1. Create package in `config/` with struct + `ValidateAndSetDefaults()` method
|
||||||
|
2. Add field to `Config` struct in `config/config.go`
|
||||||
|
3. Add `Validate*` call in `parseAndValidateConfigBytes` — **respect ordering constraints** (see comments in that function)
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- Vue 3 Composition API (`<script setup>`) + Tailwind CSS
|
||||||
|
- Props-based data flow — **do not use provide/inject**
|
||||||
|
- All components must include `dark:` prefixed Tailwind classes for dark mode
|
||||||
|
- `window.config` contains server-injected UI settings (Go template placeholders)
|
||||||
|
|
||||||
|
## API Routes (`api/api.go`)
|
||||||
|
|
||||||
|
- Unprotected routes are registered **before** `ApplySecurityMiddleware`
|
||||||
|
- Protected routes are registered **after** — placement order matters
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Config structs should have `*_test.go` files
|
||||||
|
- Use `t.Parallel()` where appropriate
|
||||||
|
- API tests spin up test servers (see existing `*_test.go` in `api/`)
|
||||||
|
|
||||||
|
## Commits & PRs
|
||||||
|
|
||||||
|
When creating a commit or PR as an agent, state that it was made by an agent and include your model name and version.
|
||||||
@@ -25,12 +25,15 @@ test:
|
|||||||
# Docker #
|
# Docker #
|
||||||
##########
|
##########
|
||||||
|
|
||||||
|
.PHONY: docker-build
|
||||||
docker-build:
|
docker-build:
|
||||||
docker build -t twinproduction/gatus:latest .
|
docker build -t twinproduction/gatus:latest .
|
||||||
|
|
||||||
|
.PHONY: docker-run
|
||||||
docker-run:
|
docker-run:
|
||||||
docker run -p 8080:8080 --name gatus twinproduction/gatus:latest
|
docker run -p 8080:8080 --name gatus twinproduction/gatus:latest
|
||||||
|
|
||||||
|
.PHONY: docker-build-and-run
|
||||||
docker-build-and-run: docker-build docker-run
|
docker-build-and-run: docker-build docker-run
|
||||||
|
|
||||||
|
|
||||||
@@ -38,11 +41,14 @@ docker-build-and-run: docker-build docker-run
|
|||||||
# Front end #
|
# Front end #
|
||||||
#############
|
#############
|
||||||
|
|
||||||
frontend-install-dependencies:
|
.PHONY: frontend-install
|
||||||
|
frontend-install:
|
||||||
npm --prefix web/app install
|
npm --prefix web/app install
|
||||||
|
|
||||||
|
.PHONY: frontend-build
|
||||||
frontend-build:
|
frontend-build:
|
||||||
npm --prefix web/app run build
|
npm --prefix web/app run build
|
||||||
|
|
||||||
frontend-run:
|
.PHONY: frontend-dev
|
||||||
|
frontend-dev:
|
||||||
npm --prefix web/app run serve
|
npm --prefix web/app run serve
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||

|

|
||||||
[](https://goreportcard.com/report/github.com/TwiN/gatus)
|
[](https://goreportcard.com/report/github.com/TwiN/gatus)
|
||||||
[](https://codecov.io/gh/TwiN/gatus)
|
|
||||||
[](https://github.com/TwiN/gatus)
|
[](https://github.com/TwiN/gatus)
|
||||||
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
[](https://cloud.docker.com/repository/docker/twinproduction/gatus)
|
||||||
[](https://github.com/TwiN)
|
[](https://github.com/TwiN)
|
||||||
@@ -303,6 +302,7 @@ You can then configure alerts to be triggered when an endpoint is unhealthy once
|
|||||||
| `endpoints[].ui.hide-url` | Whether to hide the URL from the results. Useful if the URL contains a token. | `false` |
|
| `endpoints[].ui.hide-url` | Whether to hide the URL from the results. Useful if the URL contains a token. | `false` |
|
||||||
| `endpoints[].ui.hide-errors` | Whether to hide errors from the results. | `false` |
|
| `endpoints[].ui.hide-errors` | Whether to hide errors from the results. | `false` |
|
||||||
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
| `endpoints[].ui.dont-resolve-failed-conditions` | Whether to resolve failed conditions for the UI. | `false` |
|
||||||
|
| `endpoints[].ui.resolve-successful-conditions` | Whether to resolve successful conditions for the UI (helpful to expose body assertions even when checks pass). | `false` |
|
||||||
| `endpoints[].ui.badge.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
| `endpoints[].ui.badge.response-time` | List of response time thresholds. Each time a threshold is reached, the badge has a different color. | `[50, 200, 300, 500, 750]` |
|
||||||
| `endpoints[].extra-labels` | Extra labels to add to the metrics. Useful for grouping endpoints together. | `{}` |
|
| `endpoints[].extra-labels` | Extra labels to add to the metrics. Useful for grouping endpoints together. | `{}` |
|
||||||
| `endpoints[].always-run` | (SUITES ONLY) Whether to execute this endpoint even if previous endpoints in the suite failed. | `false` |
|
| `endpoints[].always-run` | (SUITES ONLY) Whether to execute this endpoint even if previous endpoints in the suite failed. | `false` |
|
||||||
@@ -535,9 +535,12 @@ Allows you to configure the application wide defaults for the dashboard's UI. So
|
|||||||
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
| `ui.description` | Meta description for the page. | `Gatus is an advanced...`. |
|
||||||
| `ui.dashboard-heading` | Dashboard title between header and endpoints | `Health Dashboard` |
|
| `ui.dashboard-heading` | Dashboard title between header and endpoints | `Health Dashboard` |
|
||||||
| `ui.dashboard-subheading` | Dashboard description between header and endpoints | `Monitor the health of your endpoints in real-time` |
|
| `ui.dashboard-subheading` | Dashboard description between header and endpoints | `Monitor the health of your endpoints in real-time` |
|
||||||
| `ui.header` | Header at the top of the dashboard. | `Gatus` |
|
| `ui.header` | Header at the top of the dashboard. Also used as the title on the OIDC login page. | `Gatus` |
|
||||||
| `ui.logo` | URL to the logo to display. | `""` |
|
| `ui.logo` | URL to the logo to display. When set, shown alongside the Gatus logo on the OIDC login page. | `""` |
|
||||||
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
| `ui.link` | Link to open when the logo is clicked. | `""` |
|
||||||
|
| `ui.favicon.default` | Favourite default icon to display in web browser tab or address bar. | `/favicon.ico` |
|
||||||
|
| `ui.favicon.size16x16` | Favourite icon to display in web browser for 16x16 size. | `/favicon-16x16.png` |
|
||||||
|
| `ui.favicon.size32x32` | Favourite icon to display in web browser for 32x32 size. | `/favicon-32x32.png` |
|
||||||
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
| `ui.buttons` | List of buttons to display below the header. | `[]` |
|
||||||
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
|
||||||
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
|
||||||
@@ -545,6 +548,7 @@ Allows you to configure the application wide defaults for the dashboard's UI. So
|
|||||||
| `ui.dark-mode` | Whether to enable dark mode by default. Note that this is superseded by the user's operating system theme preferences. | `true` |
|
| `ui.dark-mode` | Whether to enable dark mode by default. Note that this is superseded by the user's operating system theme preferences. | `true` |
|
||||||
| `ui.default-sort-by` | Default sorting option for endpoints in the dashboard. Can be `name`, `group`, or `health`. Note that user preferences override this. | `name` |
|
| `ui.default-sort-by` | Default sorting option for endpoints in the dashboard. Can be `name`, `group`, or `health`. Note that user preferences override this. | `name` |
|
||||||
| `ui.default-filter-by` | Default filter option for endpoints in the dashboard. Can be `none`, `failing`, or `unstable`. Note that user preferences override this. | `none` |
|
| `ui.default-filter-by` | Default filter option for endpoints in the dashboard. Can be `none`, `failing`, or `unstable`. Note that user preferences override this. | `none` |
|
||||||
|
| `ui.login-subtitle` | Subtitle displayed on the OIDC login page. | `System Monitoring Dashboard` |
|
||||||
|
|
||||||
### Announcements
|
### Announcements
|
||||||
System-wide announcements allow you to display important messages at the top of the status page. These can be used to inform users about planned maintenance, ongoing issues, or general information. You can use markdown to format your announcements.
|
System-wide announcements allow you to display important messages at the top of the status page. These can be used to inform users about planned maintenance, ongoing issues, or general information. You can use markdown to format your announcements.
|
||||||
@@ -650,6 +654,7 @@ the client used to send the request.
|
|||||||
| `client.tls.renegotiation` | Type of renegotiation support to provide. (`never`, `freely`, `once`). | `"never"` |
|
| `client.tls.renegotiation` | Type of renegotiation support to provide. (`never`, `freely`, `once`). | `"never"` |
|
||||||
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
|
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
|
||||||
| `client.tunnel` | Name of the SSH tunnel to use for this endpoint. See [Tunneling](#tunneling). | `""` |
|
| `client.tunnel` | Name of the SSH tunnel to use for this endpoint. See [Tunneling](#tunneling). | `""` |
|
||||||
|
| `client.store-cookies` | Whether to store cookies between requests. | `false` |
|
||||||
|
|
||||||
|
|
||||||
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
|
||||||
@@ -662,6 +667,7 @@ client:
|
|||||||
insecure: false
|
insecure: false
|
||||||
ignore-redirect: false
|
ignore-redirect: false
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
store-cookies: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
Note that this configuration is only available under `endpoints[]`, `alerting.mattermost` and `alerting.custom`.
|
||||||
@@ -1298,6 +1304,10 @@ Here's an example of what the notifications look like:
|
|||||||
|
|
||||||
|
|
||||||
#### Configuring HomeAssistant alerts
|
#### Configuring HomeAssistant alerts
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> It is recommended to use the native Home Assistant integration instead (see below).
|
||||||
|
|
||||||
| Parameter | Description | Default Value |
|
| Parameter | Description | Default Value |
|
||||||
|:-------------------------------------------|:---------------------------------------------------------------------------------------|:--------------|
|
|:-------------------------------------------|:---------------------------------------------------------------------------------------|:--------------|
|
||||||
| `alerting.homeassistant.url` | HomeAssistant instance URL | Required `""` |
|
| `alerting.homeassistant.url` | HomeAssistant instance URL | Required `""` |
|
||||||
@@ -1372,6 +1382,61 @@ To get your HomeAssistant long-lived access token:
|
|||||||
5. Give it a name (e.g., "Gatus")
|
5. Give it a name (e.g., "Gatus")
|
||||||
6. Copy the token - you'll only see it once!
|
6. Copy the token - you'll only see it once!
|
||||||
|
|
||||||
|
##### Native Home Assistant Integration
|
||||||
|
|
||||||
|
Gatus can be integrated into [Home Assistant](https://www.home-assistant.io/) to monitor the status of your endpoints directly from your home automation dashboard.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=gatus)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Configuration & Automation Examples</summary>
|
||||||
|
|
||||||
|
###### Configuration
|
||||||
|
|
||||||
|
To add the integration, click the button above or:
|
||||||
|
|
||||||
|
1. In Home Assistant, navigate to **Settings** > **Devices & Services**.
|
||||||
|
2. Click **Add Integration** in the bottom-right corner.
|
||||||
|
3. Search for **Gatus** and select it.
|
||||||
|
4. Enter the base URL of your Gatus instance (e.g., `http://192.168.1.50:8080`).
|
||||||
|
|
||||||
|
Home Assistant will create a binary sensor for each endpoint configured in Gatus. Because these sensors use the `connectivity` device class, they will show as `Connected` (Up) when the endpoint is healthy, and `Disconnected` (Down) when the endpoint is unhealthy.
|
||||||
|
|
||||||
|
###### Automation Example
|
||||||
|
|
||||||
|
You can easily set up a UI automation in Home Assistant to receive mobile notifications if any endpoint goes down:
|
||||||
|
|
||||||
|
1. Navigate to **Settings** > **Automations & Scenes** > **Create Automation** > **Create new automation**.
|
||||||
|
2. Under **Triggers**, click **Add Trigger** and select **State**.
|
||||||
|
3. In the **Entity** field, select the Gatus binary sensor(s) you want to monitor.
|
||||||
|
4. Set the **To** field to `off` (since a state of `off` represents a disconnected/down endpoint).
|
||||||
|
5. Under **Actions**, click **Add Action** and select **Perform action**.
|
||||||
|
6. Select your notification service (e.g., `Send a notification via mobile_app_<your_device_name>`).
|
||||||
|
7. Set the **Message** field. You can use template values to dynamically include the endpoint name:
|
||||||
|
```yaml
|
||||||
|
message: "The endpoint {{ trigger.to_state.name }} is down!"
|
||||||
|
```
|
||||||
|
|
||||||
|
For advanced users, here is the YAML representation of the automation:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: "Notify when Gatus endpoint goes down"
|
||||||
|
description: "Sends a mobile notification if a Gatus endpoint goes down"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- binary_sensor.website
|
||||||
|
- binary_sensor.make_sure_header_is_rendered
|
||||||
|
to: "off"
|
||||||
|
action:
|
||||||
|
- action: notify.mobile_app_your_device_name
|
||||||
|
data:
|
||||||
|
title: "Gatus Alert"
|
||||||
|
message: "The endpoint {{ trigger.to_state.name }} is down!"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
#### Configuring IFTTT alerts
|
#### Configuring IFTTT alerts
|
||||||
|
|
||||||
@@ -2837,12 +2902,13 @@ This is an experimental feature. It may be removed or updated in a breaking mann
|
|||||||
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).
|
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.
|
Use at your own risk.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:-----------------------------------|:---------------------------------------------|:--------------|
|
|:-----------------------------------|:-----------------------------------------------|:--------------|
|
||||||
| `remote` | Remote configuration | `{}` |
|
| `remote` | Remote configuration | `{}` |
|
||||||
| `remote.instances` | List of remote instances | Required `[]` |
|
| `remote.instances` | List of remote instances | Required `[]` |
|
||||||
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
| `remote.instances.endpoint-prefix` | String to prefix all endpoint names with | `""` |
|
||||||
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
| `remote.instances.url` | URL from which to retrieve endpoint statuses | Required `""` |
|
||||||
|
| `remote.client` | [Client configuration](#client-configuration). | `{}` |
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
remote:
|
remote:
|
||||||
@@ -2901,6 +2967,7 @@ To get more details, please check [chart's configuration](https://github.com/Twi
|
|||||||
|
|
||||||
Gatus can be deployed on Kubernetes using Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
Gatus can be deployed on Kubernetes using Terraform by using the following module: [terraform-kubernetes-gatus](https://github.com/TwiN/terraform-kubernetes-gatus).
|
||||||
|
|
||||||
|
|
||||||
## Running the tests
|
## Running the tests
|
||||||
```console
|
```console
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
@@ -3252,7 +3319,7 @@ endpoints:
|
|||||||
|
|
||||||
|
|
||||||
### Concurrency
|
### Concurrency
|
||||||
By default, Gatus allows up to 5 endpoints/suites to be monitored concurrently. This provides a balance between performance and resource usage while maintaining accurate response time measurements.
|
By default, Gatus allows up to 3 endpoints/suites to be monitored concurrently. This provides a balance between performance and resource usage while maintaining accurate response time measurements.
|
||||||
|
|
||||||
You can configure the concurrency level using the `concurrency` parameter:
|
You can configure the concurrency level using the `concurrency` parameter:
|
||||||
|
|
||||||
|
|||||||
@@ -143,10 +143,10 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
|
|||||||
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
|
||||||
var subject, message string
|
var subject, message string
|
||||||
if resolved {
|
if resolved {
|
||||||
subject = fmt.Sprintf("[%s] Alert resolved", ep.DisplayName())
|
subject = fmt.Sprintf("%s: Alert resolved", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("[%s] Alert triggered", ep.DisplayName())
|
subject = fmt.Sprintf("%s: Alert triggered", ep.DisplayName())
|
||||||
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
|
||||||
}
|
}
|
||||||
var formattedConditionResults string
|
var formattedConditionResults string
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
Endpoint: &endpoint.Endpoint{Name: "endpoint-name"},
|
Endpoint: &endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
ExpectedSubject: "[endpoint-name] Alert triggered",
|
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",
|
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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
Endpoint: &endpoint.Endpoint{Name: "endpoint-name"},
|
Endpoint: &endpoint.Endpoint{Name: "endpoint-name"},
|
||||||
ExpectedSubject: "[endpoint-name] Alert resolved",
|
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",
|
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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +104,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{"environment": "production"}},
|
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{"environment": "production"}},
|
||||||
ExpectedSubject: "[endpoint-name] Alert triggered",
|
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\nExtra labels:\n environment: production\n\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
|
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\nExtra labels:\n environment: production\n\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -113,7 +113,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: true,
|
Resolved: true,
|
||||||
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{"service": "api"}},
|
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{"service": "api"}},
|
||||||
ExpectedSubject: "[endpoint-name] Alert resolved",
|
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\nExtra labels:\n service: api\n\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
|
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nExtra labels:\n service: api\n\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -122,7 +122,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
|
|||||||
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
Resolved: false,
|
Resolved: false,
|
||||||
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{}},
|
Endpoint: &endpoint.Endpoint{Name: "endpoint-name", ExtraLabels: map[string]string{}},
|
||||||
ExpectedSubject: "[endpoint-name] Alert triggered",
|
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",
|
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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isIgnorableTestError checks if an error is expected during testing when making API calls with dummy credentials
|
||||||
|
func isIgnorableTestError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
errStr := err.Error()
|
||||||
|
return strings.Contains(errStr, "user does not exist") ||
|
||||||
|
strings.Contains(errStr, "no such host") ||
|
||||||
|
strings.Contains(errStr, "invalid username, password or token") ||
|
||||||
|
strings.Contains(errStr, "unknown API error") ||
|
||||||
|
strings.Contains(errStr, "dial tcp")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAlertProvider_Validate(t *testing.T) {
|
func TestAlertProvider_Validate(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -50,7 +63,7 @@ func TestAlertProvider_Validate(t *testing.T) {
|
|||||||
if scenario.ExpectedError && err == nil {
|
if scenario.ExpectedError && err == nil {
|
||||||
t.Error("expected error, got none")
|
t.Error("expected error, got none")
|
||||||
}
|
}
|
||||||
if !scenario.ExpectedError && err != nil && !strings.Contains(err.Error(), "user does not exist") && !strings.Contains(err.Error(), "no such host") {
|
if !scenario.ExpectedError && err != nil && !isIgnorableTestError(err) {
|
||||||
t.Error("expected no error, got", err.Error())
|
t.Error("expected no error, got", err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -87,7 +100,7 @@ func TestAlertProvider_Send(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
cfg, err := scenario.Provider.GetConfig("", &scenario.Alert)
|
cfg, err := scenario.Provider.GetConfig("", &scenario.Alert)
|
||||||
if err != nil && !strings.Contains(err.Error(), "user does not exist") && !strings.Contains(err.Error(), "no such host") {
|
if err != nil && !isIgnorableTestError(err) {
|
||||||
t.Error("expected no error, got", err.Error())
|
t.Error("expected no error, got", err.Error())
|
||||||
}
|
}
|
||||||
cfg.giteaClient, _ = gitea.NewClient("https://gitea.com")
|
cfg.giteaClient, _ = gitea.NewClient("https://gitea.com")
|
||||||
@@ -203,7 +216,7 @@ func TestAlertProvider_GetConfig(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
got, err := scenario.Provider.GetConfig("", &scenario.InputAlert)
|
got, err := scenario.Provider.GetConfig("", &scenario.InputAlert)
|
||||||
if err != nil && !strings.Contains(err.Error(), "user does not exist") && !strings.Contains(err.Error(), "no such host") {
|
if err != nil && !isIgnorableTestError(err) {
|
||||||
t.Fatalf("unexpected error: %s", err)
|
t.Fatalf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
if got.RepositoryURL != scenario.ExpectedOutput.RepositoryURL {
|
if got.RepositoryURL != scenario.ExpectedOutput.RepositoryURL {
|
||||||
@@ -221,7 +234,7 @@ func TestAlertProvider_GetConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Test ValidateOverrides as well, since it really just calls GetConfig
|
// Test ValidateOverrides as well, since it really just calls GetConfig
|
||||||
if err = scenario.Provider.ValidateOverrides("", &scenario.InputAlert); err != nil && !strings.Contains(err.Error(), "user does not exist") {
|
if err = scenario.Provider.ValidateOverrides("", &scenario.InputAlert); err != nil && !isIgnorableTestError(err) {
|
||||||
t.Errorf("unexpected error: %s", err)
|
t.Errorf("unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -170,9 +171,7 @@ func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoi
|
|||||||
// Merge metadata: cfg.Metadata + ep.ExtraLabels (if present)
|
// Merge metadata: cfg.Metadata + ep.ExtraLabels (if present)
|
||||||
mergedMetadata := map[string]interface{}{}
|
mergedMetadata := map[string]interface{}{}
|
||||||
// Copy cfg.Metadata
|
// Copy cfg.Metadata
|
||||||
for k, v := range cfg.Metadata {
|
maps.Copy(mergedMetadata, cfg.Metadata)
|
||||||
mergedMetadata[k] = v
|
|
||||||
}
|
|
||||||
// Add extra labels from endpoint (if present)
|
// Add extra labels from endpoint (if present)
|
||||||
if ep.ExtraLabels != nil && len(ep.ExtraLabels) > 0 {
|
if ep.ExtraLabels != nil && len(ep.ExtraLabels) > 0 {
|
||||||
for k, v := range ep.ExtraLabels {
|
for k, v := range ep.ExtraLabels {
|
||||||
|
|||||||
+1
-1
@@ -299,7 +299,7 @@ func getBadgeColorFromResponseTime(responseTime int, key string, cfg *config.Con
|
|||||||
thresholds = endpoint.UIConfig.Badge.ResponseTime.Thresholds
|
thresholds = endpoint.UIConfig.Badge.ResponseTime.Thresholds
|
||||||
}
|
}
|
||||||
// the threshold config requires 5 values, so we can be sure it's set here
|
// the threshold config requires 5 values, so we can be sure it's set here
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
if responseTime <= thresholds[i] {
|
if responseTime <= thresholds[i] {
|
||||||
return badgeColors[i]
|
return badgeColors[i]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwiN/gatus/v5/security"
|
"github.com/TwiN/gatus/v5/security"
|
||||||
@@ -27,7 +28,7 @@ func TestConfigHandler_ServeHTTP(t *testing.T) {
|
|||||||
t.Error("expected err to be nil, but was", err)
|
t.Error("expected err to be nil, but was", err)
|
||||||
}
|
}
|
||||||
// Test the config handler
|
// Test the config handler
|
||||||
request, _ := http.NewRequest("GET", "/api/v1/config", http.NoBody)
|
request := httptest.NewRequest("GET", "/api/v1/config", http.NoBody)
|
||||||
response, err := app.Test(request)
|
response, err := app.Test(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("expected err to be nil, but was", err)
|
t.Error("expected err to be nil, but was", err)
|
||||||
|
|||||||
@@ -499,6 +499,13 @@ func QueryDNS(queryType, queryName, url string) (connected bool, dnsRcode string
|
|||||||
if srv, ok := rr.(*dns.SRV); ok {
|
if srv, ok := rr.(*dns.SRV); ok {
|
||||||
body = []byte(fmt.Sprintf("%s:%d", srv.Target, srv.Port))
|
body = []byte(fmt.Sprintf("%s:%d", srv.Target, srv.Port))
|
||||||
}
|
}
|
||||||
|
case dns.TypeTXT:
|
||||||
|
if txt, ok := rr.(*dns.TXT); ok {
|
||||||
|
if len(body) > 0 {
|
||||||
|
body = append(body, '\n')
|
||||||
|
}
|
||||||
|
body = append(body, []byte(strings.Join(txt.Txt, ""))...)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
body = []byte("query type is not supported yet")
|
body = []byte("query type is not supported yet")
|
||||||
}
|
}
|
||||||
@@ -528,6 +535,9 @@ func rdapQuery(hostname string) (*whois.Response, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if response.ExpirationDate.IsZero() {
|
||||||
|
return nil, fmt.Errorf("no expiration event found in RDAP response for %s", hostname)
|
||||||
|
}
|
||||||
return &response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-4
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,8 +17,18 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isIgnorableNetworkTestError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
errString := err.Error()
|
||||||
|
return strings.Contains(errString, "no such host") ||
|
||||||
|
strings.Contains(errString, "connection reset by peer") ||
|
||||||
|
strings.Contains(errString, "i/o timeout") ||
|
||||||
|
strings.Contains(errString, "server misbehaving")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetHTTPClient(t *testing.T) {
|
func TestGetHTTPClient(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Insecure: false,
|
Insecure: false,
|
||||||
IgnoreRedirect: false,
|
IgnoreRedirect: false,
|
||||||
@@ -55,6 +66,13 @@ func TestRdapQuery(t *testing.T) {
|
|||||||
} else if response.ExpirationDate.Unix() <= 0 {
|
} else if response.ExpirationDate.Unix() <= 0 {
|
||||||
t.Error("expected to have a valid expiry date, got", response.ExpirationDate.Unix())
|
t.Error("expected to have a valid expiry date, got", response.ExpirationDate.Unix())
|
||||||
}
|
}
|
||||||
|
// .net domain: rdapQuery must either return a valid expiration or an error,
|
||||||
|
// but never silently return a zero-value expiration date (the bug in #1570)
|
||||||
|
if response, err := rdapQuery("google.net"); err != nil {
|
||||||
|
t.Logf("rdapQuery returned error for .net domain (fallback to WHOIS expected): %s", err)
|
||||||
|
} else if response.ExpirationDate.IsZero() {
|
||||||
|
t.Error("rdapQuery returned a zero expiration date without an error for .net domain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetDomainExpiration(t *testing.T) {
|
func TestGetDomainExpiration(t *testing.T) {
|
||||||
@@ -83,6 +101,12 @@ func TestGetDomainExpiration(t *testing.T) {
|
|||||||
} else if domainExpiration <= 0 {
|
} else if domainExpiration <= 0 {
|
||||||
t.Error("expected domain expiration to be higher than 0")
|
t.Error("expected domain expiration to be higher than 0")
|
||||||
}
|
}
|
||||||
|
// .net domain: should succeed via RDAP or WHOIS fallback (#1570)
|
||||||
|
if domainExpiration, err := GetDomainExpiration("google.net"); err != nil {
|
||||||
|
t.Errorf("expected error to be nil for .net domain, but got: `%s`", err)
|
||||||
|
} else if domainExpiration <= 0 {
|
||||||
|
t.Error("expected domain expiration to be higher than 0 for .net domain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
func TestPing(t *testing.T) {
|
||||||
@@ -209,6 +233,9 @@ func TestCanPerformStartTLS(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second, DNSResolver: tt.args.dnsresolver})
|
connected, _, err := CanPerformStartTLS(tt.args.address, &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second, DNSResolver: tt.args.dnsresolver})
|
||||||
|
if !tt.wantErr && isIgnorableNetworkTestError(err) {
|
||||||
|
t.Skipf("skipping due to transient network error: %v", err)
|
||||||
|
}
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
t.Errorf("CanPerformStartTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -278,6 +305,9 @@ func TestCanPerformTLS(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
connected, _, _, err := CanPerformTLS(tt.args.address, "", &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
connected, _, _, err := CanPerformTLS(tt.args.address, "", &Config{Insecure: tt.args.insecure, Timeout: 5 * time.Second})
|
||||||
|
if !tt.wantErr && isIgnorableNetworkTestError(err) {
|
||||||
|
t.Skipf("skipping due to transient network error: %v", err)
|
||||||
|
}
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
t.Errorf("CanPerformTLS() err=%v, wantErr=%v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
@@ -305,7 +335,6 @@ func TestCanCreateConnection(t *testing.T) {
|
|||||||
// performs a Client Credentials OAuth2 flow and adds the obtained token as a `Authorization`
|
// performs a Client Credentials OAuth2 flow and adds the obtained token as a `Authorization`
|
||||||
// header to all outgoing HTTP calls.
|
// header to all outgoing HTTP calls.
|
||||||
func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
func TestHttpClientProvidesOAuth2BearerToken(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
defer InjectHTTPClient(nil)
|
defer InjectHTTPClient(nil)
|
||||||
oAuth2Config := &OAuth2Config{
|
oAuth2Config := &OAuth2Config{
|
||||||
ClientID: "00000000-0000-0000-0000-000000000000",
|
ClientID: "00000000-0000-0000-0000-000000000000",
|
||||||
@@ -496,6 +525,16 @@ func TestQueryDNS(t *testing.T) {
|
|||||||
expectedDNSCode: "NOERROR",
|
expectedDNSCode: "NOERROR",
|
||||||
expectedBody: "one.one.one.one.",
|
expectedBody: "one.one.one.one.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test Config with type TXT",
|
||||||
|
inputDNS: dns.Config{
|
||||||
|
QueryType: "TXT",
|
||||||
|
QueryName: "example.com.",
|
||||||
|
},
|
||||||
|
inputURL: "1.1.1.1",
|
||||||
|
expectedDNSCode: "NOERROR",
|
||||||
|
expectedBody: "*v=spf1*",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "test Config with fake type and retrieve error",
|
name: "test Config with fake type and retrieve error",
|
||||||
inputDNS: dns.Config{
|
inputDNS: dns.Config{
|
||||||
@@ -515,8 +554,8 @@ func TestQueryDNS(t *testing.T) {
|
|||||||
if dnsRCode != scenario.expectedDNSCode {
|
if dnsRCode != scenario.expectedDNSCode {
|
||||||
t.Errorf("expected DNSRCode to be %s, got %s", scenario.expectedDNSCode, dnsRCode)
|
t.Errorf("expected DNSRCode to be %s, got %s", scenario.expectedDNSCode, dnsRCode)
|
||||||
}
|
}
|
||||||
if scenario.inputDNS.QueryType == "NS" {
|
if scenario.inputDNS.QueryType == "NS" || scenario.inputDNS.QueryType == "TXT" {
|
||||||
// Because there are often multiple nameservers backing a single domain, we'll only look at the suffix
|
// Some record types can have multiple valid answers, so wildcard matching is used in those scenarios
|
||||||
if !pattern.Match(scenario.expectedBody, string(body)) {
|
if !pattern.Match(scenario.expectedBody, string(body)) {
|
||||||
t.Errorf("got %s, expected result %s,", string(body), scenario.expectedBody)
|
t.Errorf("got %s, expected result %s,", string(body), scenario.expectedBody)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -34,6 +35,7 @@ var (
|
|||||||
IgnoreRedirect: false,
|
IgnoreRedirect: false,
|
||||||
Timeout: defaultTimeout,
|
Timeout: defaultTimeout,
|
||||||
Network: "ip",
|
Network: "ip",
|
||||||
|
StoreCookies: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,6 +84,9 @@ type Config struct {
|
|||||||
// ResolvedTunnel is the resolved SSH tunnel for this specific Config
|
// ResolvedTunnel is the resolved SSH tunnel for this specific Config
|
||||||
ResolvedTunnel *sshtunnel.SSHTunnel `yaml:"-"`
|
ResolvedTunnel *sshtunnel.SSHTunnel `yaml:"-"`
|
||||||
|
|
||||||
|
// StoreCookies determines whether cookies are stored and included across requests.
|
||||||
|
StoreCookies bool `yaml:"store-cookies,omitempty"`
|
||||||
|
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +285,13 @@ func (c *Config) getHTTPClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.StoreCookies {
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
logr.Fatalf("[client.getHTTPClient] Failed to initialize cookie jar: %v", err)
|
||||||
|
}
|
||||||
|
c.httpClient.Jar = jar
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c.httpClient
|
return c.httpClient
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
|||||||
if insecureClient.Timeout != defaultTimeout {
|
if insecureClient.Timeout != defaultTimeout {
|
||||||
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
|
t.Error("expected Config.Timeout to default the HTTP client to a timeout of 10s")
|
||||||
}
|
}
|
||||||
|
if insecureClient.Jar != nil {
|
||||||
|
t.Error("expected Config.StoreCookies to default the HTTP client to not store cookies")
|
||||||
|
}
|
||||||
request, _ := http.NewRequest("GET", "", nil)
|
request, _ := http.NewRequest("GET", "", nil)
|
||||||
if err := insecureClient.CheckRedirect(request, nil); err != nil {
|
if err := insecureClient.CheckRedirect(request, nil); err != nil {
|
||||||
t.Error("expected Config.IgnoreRedirect set to false to cause the HTTP client's CheckRedirect to return nil")
|
t.Error("expected Config.IgnoreRedirect set to false to cause the HTTP client's CheckRedirect to return nil")
|
||||||
@@ -31,12 +34,24 @@ func TestConfig_getHTTPClient(t *testing.T) {
|
|||||||
if secureClient.Timeout != 5*time.Second {
|
if secureClient.Timeout != 5*time.Second {
|
||||||
t.Error("expected Config.Timeout to cause the HTTP client to have a timeout of 5s")
|
t.Error("expected Config.Timeout to cause the HTTP client to have a timeout of 5s")
|
||||||
}
|
}
|
||||||
|
if insecureClient.Jar != nil {
|
||||||
|
t.Error("expected Config.StoreCookies to default the HTTP client to not store cookies")
|
||||||
|
}
|
||||||
request, _ = http.NewRequest("GET", "", nil)
|
request, _ = http.NewRequest("GET", "", nil)
|
||||||
if err := secureClient.CheckRedirect(request, nil); err != http.ErrUseLastResponse {
|
if err := secureClient.CheckRedirect(request, nil); err != http.ErrUseLastResponse {
|
||||||
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
|
t.Error("expected Config.IgnoreRedirect set to true to cause the HTTP client's CheckRedirect to return http.ErrUseLastResponse")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_getHTTPClient_withCookieJar(t *testing.T) {
|
||||||
|
config := &Config{StoreCookies: true}
|
||||||
|
config.ValidateAndSetDefaults()
|
||||||
|
client := config.getHTTPClient()
|
||||||
|
if client.Jar == nil {
|
||||||
|
t.Error("expected Config.StoreCookies to cause the HTTP client to have a CookieJar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
func TestConfig_ValidateAndSetDefaults_withCustomDNSResolver(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
dnsResolver string
|
dnsResolver string
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -136,7 +137,7 @@ func (config *Config) GetUniqueExtraMetricLabels() []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for label := range ep.ExtraLabels {
|
for label := range ep.ExtraLabels {
|
||||||
if contains(labels, label) {
|
if slices.Contains(labels, label) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
labels = append(labels, label)
|
labels = append(labels, label)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -2052,7 +2053,7 @@ func TestConfig_GetUniqueExtraMetricLabels(t *testing.T) {
|
|||||||
t.Errorf("expected %d labels, got %d", len(tt.expected), len(labels))
|
t.Errorf("expected %d labels, got %d", len(tt.expected), len(labels))
|
||||||
}
|
}
|
||||||
for _, label := range tt.expected {
|
for _, label := range tt.expected {
|
||||||
if !contains(labels, label) {
|
if !slices.Contains(labels, label) {
|
||||||
t.Errorf("expected label %s to be present", label)
|
t.Errorf("expected label %s to be present", label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type Condition string
|
|||||||
// Validate checks if the Condition is valid
|
// Validate checks if the Condition is valid
|
||||||
func (c Condition) Validate() error {
|
func (c Condition) Validate() error {
|
||||||
r := &Result{}
|
r := &Result{}
|
||||||
c.evaluate(r, false, nil)
|
c.evaluate(r, false, false, nil)
|
||||||
if len(r.Errors) != 0 {
|
if len(r.Errors) != 0 {
|
||||||
return errors.New(r.Errors[0])
|
return errors.New(r.Errors[0])
|
||||||
}
|
}
|
||||||
@@ -34,44 +34,50 @@ func (c Condition) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// evaluate the Condition with the Result and an optional context
|
// evaluate the Condition with the Result and an optional context
|
||||||
func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool, context *gontext.Gontext) bool {
|
func (c Condition) evaluate(result *Result, dontResolveFailedConditions bool, resolveSuccessfulConditions bool, context *gontext.Gontext) bool {
|
||||||
condition := string(c)
|
condition := string(c)
|
||||||
success := false
|
success := false
|
||||||
conditionToDisplay := condition
|
conditionToDisplay := condition
|
||||||
|
shouldResolveCondition := func(success bool) bool {
|
||||||
|
if success {
|
||||||
|
return resolveSuccessfulConditions
|
||||||
|
}
|
||||||
|
return !dontResolveFailedConditions
|
||||||
|
}
|
||||||
if strings.Contains(condition, " == ") {
|
if strings.Contains(condition, " == ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " == "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " == "), result, context)
|
||||||
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
success = isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
conditionToDisplay = prettify(parameters, resolvedParameters, "==")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, " != ") {
|
} else if strings.Contains(condition, " != ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " != "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveWithContext(strings.Split(condition, " != "), result, context)
|
||||||
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
success = !isEqual(resolvedParameters[0], resolvedParameters[1])
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
conditionToDisplay = prettify(parameters, resolvedParameters, "!=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, " <= ") {
|
} else if strings.Contains(condition, " <= ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " <= "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " <= "), result, context)
|
||||||
success = resolvedParameters[0] <= resolvedParameters[1]
|
success = resolvedParameters[0] <= resolvedParameters[1]
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, " >= ") {
|
} else if strings.Contains(condition, " >= ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " >= "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " >= "), result, context)
|
||||||
success = resolvedParameters[0] >= resolvedParameters[1]
|
success = resolvedParameters[0] >= resolvedParameters[1]
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">=")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, " > ") {
|
} else if strings.Contains(condition, " > ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " > "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " > "), result, context)
|
||||||
success = resolvedParameters[0] > resolvedParameters[1]
|
success = resolvedParameters[0] > resolvedParameters[1]
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, ">")
|
||||||
}
|
}
|
||||||
} else if strings.Contains(condition, " < ") {
|
} else if strings.Contains(condition, " < ") {
|
||||||
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " < "), result, context)
|
parameters, resolvedParameters := sanitizeAndResolveNumericalWithContext(strings.Split(condition, " < "), result, context)
|
||||||
success = resolvedParameters[0] < resolvedParameters[1]
|
success = resolvedParameters[0] < resolvedParameters[1]
|
||||||
if !success && !dontResolveFailedConditions {
|
if shouldResolveCondition(success) {
|
||||||
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
conditionToDisplay = prettifyNumericalParameters(parameters, resolvedParameters, "<")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -210,7 +216,7 @@ func sanitizeAndResolveNumericalWithContext(list []string, result *Result, conte
|
|||||||
|
|
||||||
func prettifyNumericalParameters(parameters []string, resolvedParameters []int64, operator string) string {
|
func prettifyNumericalParameters(parameters []string, resolvedParameters []int64, operator string) string {
|
||||||
resolvedStrings := make([]string, 2)
|
resolvedStrings := make([]string, 2)
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
// Check if the parameter is a certificate or domain expiration placeholder
|
// Check if the parameter is a certificate or domain expiration placeholder
|
||||||
if parameters[i] == CertificateExpirationPlaceholder || parameters[i] == DomainExpirationPlaceholder {
|
if parameters[i] == CertificateExpirationPlaceholder || parameters[i] == DomainExpirationPlaceholder {
|
||||||
// Format as duration string (convert milliseconds back to duration)
|
// Format as duration string (convert milliseconds back to duration)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ func BenchmarkCondition_evaluateWithBodyStringAny(b *testing.B) {
|
|||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ func BenchmarkCondition_evaluateWithBodyStringAnyFailure(b *testing.B) {
|
|||||||
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
condition := Condition("[BODY].name == any(john.doe, jane.doe)")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ func BenchmarkCondition_evaluateWithBodyString(b *testing.B) {
|
|||||||
condition := Condition("[BODY].name == john.doe")
|
condition := Condition("[BODY].name == john.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailure(b *testing.B) {
|
|||||||
condition := Condition("[BODY].name == john.doe")
|
condition := Condition("[BODY].name == john.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func BenchmarkCondition_evaluateWithBodyStringFailureInvalidPath(b *testing.B) {
|
|||||||
condition := Condition("[BODY].user.name == bob.doe")
|
condition := Condition("[BODY].user.name == bob.doe")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func BenchmarkCondition_evaluateWithBodyStringLen(b *testing.B) {
|
|||||||
condition := Condition("len([BODY].name) == 8")
|
condition := Condition("len([BODY].name) == 8")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"john.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func BenchmarkCondition_evaluateWithBodyStringLenFailure(b *testing.B) {
|
|||||||
condition := Condition("len([BODY].name) == 8")
|
condition := Condition("len([BODY].name) == 8")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
result := &Result{Body: []byte("{\"name\": \"bob.doe\"}")}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ func BenchmarkCondition_evaluateWithStatus(b *testing.B) {
|
|||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{HTTPStatus: 200}
|
result := &Result{HTTPStatus: 200}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func BenchmarkCondition_evaluateWithStatusFailure(b *testing.B) {
|
|||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
result := &Result{HTTPStatus: 400}
|
result := &Result{HTTPStatus: 400}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func TestCondition_evaluate(t *testing.T) {
|
|||||||
Condition Condition
|
Condition Condition
|
||||||
Result *Result
|
Result *Result
|
||||||
DontResolveFailedConditions bool
|
DontResolveFailedConditions bool
|
||||||
|
ResolveSuccessfulConditions bool
|
||||||
ExpectedSuccess bool
|
ExpectedSuccess bool
|
||||||
ExpectedOutput string
|
ExpectedOutput string
|
||||||
}{
|
}{
|
||||||
@@ -184,6 +185,14 @@ func TestCondition_evaluate(t *testing.T) {
|
|||||||
ExpectedSuccess: true,
|
ExpectedSuccess: true,
|
||||||
ExpectedOutput: "[BODY] == test",
|
ExpectedOutput: "[BODY] == test",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "body-resolved-on-success",
|
||||||
|
Condition: Condition("[BODY].status == UP"),
|
||||||
|
Result: &Result{Body: []byte("{\"status\":\"UP\"}")},
|
||||||
|
ResolveSuccessfulConditions: true,
|
||||||
|
ExpectedSuccess: true,
|
||||||
|
ExpectedOutput: "[BODY].status (UP) == UP",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "body-numerical-equal",
|
Name: "body-numerical-equal",
|
||||||
Condition: Condition("[BODY] == 123"),
|
Condition: Condition("[BODY] == 123"),
|
||||||
@@ -757,7 +766,7 @@ func TestCondition_evaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
scenario.Condition.evaluate(scenario.Result, scenario.DontResolveFailedConditions, nil)
|
scenario.Condition.evaluate(scenario.Result, scenario.DontResolveFailedConditions, scenario.ResolveSuccessfulConditions, nil)
|
||||||
if scenario.Result.ConditionResults[0].Success != scenario.ExpectedSuccess {
|
if scenario.Result.ConditionResults[0].Success != scenario.ExpectedSuccess {
|
||||||
t.Errorf("Condition '%s' should have been success=%v", scenario.Condition, scenario.ExpectedSuccess)
|
t.Errorf("Condition '%s' should have been success=%v", scenario.Condition, scenario.ExpectedSuccess)
|
||||||
}
|
}
|
||||||
@@ -771,7 +780,7 @@ func TestCondition_evaluate(t *testing.T) {
|
|||||||
func TestCondition_evaluateWithInvalidOperator(t *testing.T) {
|
func TestCondition_evaluateWithInvalidOperator(t *testing.T) {
|
||||||
condition := Condition("[STATUS] ? 201")
|
condition := Condition("[STATUS] ? 201")
|
||||||
result := &Result{HTTPStatus: 201}
|
result := &Result{HTTPStatus: 201}
|
||||||
condition.evaluate(result, false, nil)
|
condition.evaluate(result, false, false, nil)
|
||||||
if result.Success {
|
if result.Success {
|
||||||
t.Error("condition was invalid, result should've been a failure")
|
t.Error("condition was invalid, result should've been a failure")
|
||||||
}
|
}
|
||||||
@@ -791,7 +800,7 @@ func TestConditionEvaluateWithInvalidContextPlaceholder(t *testing.T) {
|
|||||||
"max_response_time": 5000,
|
"max_response_time": 5000,
|
||||||
})
|
})
|
||||||
// Simulate suite endpoint evaluation with context
|
// Simulate suite endpoint evaluation with context
|
||||||
success := condition.evaluate(result, false, ctx) // false = don't skip resolution (default)
|
success := condition.evaluate(result, false, false, ctx) // false = don't skip resolution (default)
|
||||||
if success {
|
if success {
|
||||||
t.Error("Condition should have failed because [CONTEXT].expected_statusz doesn't exist")
|
t.Error("Condition should have failed because [CONTEXT].expected_statusz doesn't exist")
|
||||||
}
|
}
|
||||||
@@ -814,7 +823,7 @@ func TestConditionEvaluateWithValidContextPlaceholder(t *testing.T) {
|
|||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
})
|
})
|
||||||
// Simulate suite endpoint evaluation with context
|
// Simulate suite endpoint evaluation with context
|
||||||
success := condition.evaluate(result, false, ctx)
|
success := condition.evaluate(result, false, false, ctx)
|
||||||
if !success {
|
if !success {
|
||||||
t.Error("Condition should have succeeded")
|
t.Error("Condition should have succeeded")
|
||||||
}
|
}
|
||||||
@@ -839,7 +848,7 @@ func TestConditionEvaluateWithMixedValidAndInvalidContext(t *testing.T) {
|
|||||||
"valid_key": 5000,
|
"valid_key": 5000,
|
||||||
})
|
})
|
||||||
// Simulate suite endpoint evaluation with context
|
// Simulate suite endpoint evaluation with context
|
||||||
success := condition.evaluate(result, false, ctx)
|
success := condition.evaluate(result, false, false, ctx)
|
||||||
if success {
|
if success {
|
||||||
t.Error("Condition should have failed because [CONTEXT].invalid_key doesn't exist")
|
t.Error("Condition should have failed because [CONTEXT].invalid_key doesn't exist")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -221,12 +222,12 @@ func (e *Endpoint) ValidateAndSetDefaults() error {
|
|||||||
e.Headers = make(map[string]string)
|
e.Headers = make(map[string]string)
|
||||||
}
|
}
|
||||||
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
// Automatically add user agent header if there isn't one specified in the endpoint configuration
|
||||||
if _, userAgentHeaderExists := e.Headers[UserAgentHeader]; !userAgentHeaderExists {
|
if !hasHeader(e.Headers, UserAgentHeader) {
|
||||||
e.Headers[UserAgentHeader] = GatusUserAgent
|
e.Headers[UserAgentHeader] = GatusUserAgent
|
||||||
}
|
}
|
||||||
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
// Automatically add "Content-Type: application/json" header if there's no Content-Type set
|
||||||
// and endpoint.GraphQL is set to true
|
// and endpoint.GraphQL is set to true
|
||||||
if _, contentTypeHeaderExists := e.Headers[ContentTypeHeader]; !contentTypeHeaderExists && e.GraphQL {
|
if !hasHeader(e.Headers, ContentTypeHeader) && e.GraphQL {
|
||||||
e.Headers[ContentTypeHeader] = "application/json"
|
e.Headers[ContentTypeHeader] = "application/json"
|
||||||
}
|
}
|
||||||
if len(e.Conditions) == 0 {
|
if len(e.Conditions) == 0 {
|
||||||
@@ -332,7 +333,7 @@ func (e *Endpoint) EvaluateHealthWithContext(context *gontext.Gontext) *Result {
|
|||||||
}
|
}
|
||||||
// Evaluate the conditions
|
// Evaluate the conditions
|
||||||
for _, condition := range processedEndpoint.Conditions {
|
for _, condition := range processedEndpoint.Conditions {
|
||||||
success := condition.evaluate(result, processedEndpoint.UIConfig.DontResolveFailedConditions, context)
|
success := condition.evaluate(result, processedEndpoint.UIConfig.DontResolveFailedConditions, processedEndpoint.UIConfig.ResolveSuccessfulConditions, context)
|
||||||
if !success {
|
if !success {
|
||||||
result.Success = false
|
result.Success = false
|
||||||
}
|
}
|
||||||
@@ -489,12 +490,10 @@ func (e *Endpoint) call(result *Result) {
|
|||||||
} else if endpointType == TypeWS {
|
} else if endpointType == TypeWS {
|
||||||
wsHeaders := map[string]string{}
|
wsHeaders := map[string]string{}
|
||||||
if e.Headers != nil {
|
if e.Headers != nil {
|
||||||
for k, v := range e.Headers {
|
maps.Copy(wsHeaders, e.Headers)
|
||||||
wsHeaders[k] = v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if _, exists := wsHeaders["User-Agent"]; !exists {
|
if !hasHeader(wsHeaders, UserAgentHeader) {
|
||||||
wsHeaders["User-Agent"] = GatusUserAgent
|
wsHeaders[UserAgentHeader] = GatusUserAgent
|
||||||
}
|
}
|
||||||
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.getParsedBody(), wsHeaders, e.ClientConfig)
|
result.Connected, result.Body, err = client.QueryWebSocket(e.URL, e.getParsedBody(), wsHeaders, e.ClientConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -582,7 +581,7 @@ func (e *Endpoint) buildHTTPRequest() *http.Request {
|
|||||||
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
request, _ := http.NewRequest(e.Method, e.URL, bodyBuffer)
|
||||||
for k, v := range e.Headers {
|
for k, v := range e.Headers {
|
||||||
request.Header.Set(k, v)
|
request.Header.Set(k, v)
|
||||||
if k == HostHeader {
|
if strings.EqualFold(k, HostHeader) {
|
||||||
request.Host = v
|
request.Host = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,3 +625,13 @@ func (e *Endpoint) needsToRetrieveIP() bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasHeader checks if a header exists in the map using a case-insensitive lookup
|
||||||
|
func hasHeader(headers map[string]string, name string) bool {
|
||||||
|
for k := range headers {
|
||||||
|
if strings.EqualFold(k, name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,30 @@ import (
|
|||||||
"github.com/TwiN/gatus/v5/test"
|
"github.com/TwiN/gatus/v5/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestHasHeader(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
headers map[string]string
|
||||||
|
lookup string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{name: "exact-match", headers: map[string]string{"User-Agent": "test"}, lookup: "User-Agent", expected: true},
|
||||||
|
{name: "lowercase-lookup", headers: map[string]string{"User-Agent": "test"}, lookup: "user-agent", expected: true},
|
||||||
|
{name: "uppercase-lookup", headers: map[string]string{"user-agent": "test"}, lookup: "USER-AGENT", expected: true},
|
||||||
|
{name: "mixed-case", headers: map[string]string{"UsEr-AgEnT": "test"}, lookup: "uSeR-aGeNt", expected: true},
|
||||||
|
{name: "not-found", headers: map[string]string{"Content-Type": "test"}, lookup: "User-Agent", expected: false},
|
||||||
|
{name: "empty-headers", headers: map[string]string{}, lookup: "User-Agent", expected: false},
|
||||||
|
{name: "nil-headers", headers: nil, lookup: "User-Agent", expected: false},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.name, func(t *testing.T) {
|
||||||
|
if result := hasHeader(scenario.headers, scenario.lookup); result != scenario.expected {
|
||||||
|
t.Errorf("expected %v, got %v", scenario.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint(t *testing.T) {
|
func TestEndpoint(t *testing.T) {
|
||||||
defer client.InjectHTTPClient(nil)
|
defer client.InjectHTTPClient(nil)
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
@@ -268,6 +292,31 @@ func TestEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_ResolveSuccessfulConditions(t *testing.T) {
|
||||||
|
defer client.InjectHTTPClient(nil)
|
||||||
|
endpoint := Endpoint{
|
||||||
|
Name: "test-endpoint",
|
||||||
|
URL: "https://example.com/health",
|
||||||
|
Conditions: []Condition{"[BODY].status == UP"},
|
||||||
|
UIConfig: &ui.Config{ResolveSuccessfulConditions: true},
|
||||||
|
}
|
||||||
|
mockResponse := test.MockRoundTripper(func(r *http.Request) *http.Response {
|
||||||
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`{"status":"UP"}`))}
|
||||||
|
})
|
||||||
|
client.InjectHTTPClient(&http.Client{Transport: mockResponse})
|
||||||
|
if err := endpoint.ValidateAndSetDefaults(); err != nil {
|
||||||
|
t.Fatalf("ValidateAndSetDefaults failed: %v", err)
|
||||||
|
}
|
||||||
|
result := endpoint.EvaluateHealth()
|
||||||
|
if len(result.ConditionResults) != 1 {
|
||||||
|
t.Fatalf("expected 1 condition result, got %d", len(result.ConditionResults))
|
||||||
|
}
|
||||||
|
expectedCondition := "[BODY].status (UP) == UP"
|
||||||
|
if result.ConditionResults[0].Condition != expectedCondition {
|
||||||
|
t.Errorf("expected condition to be '%s', got '%s'", expectedCondition, result.ConditionResults[0].Condition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_IsEnabled(t *testing.T) {
|
func TestEndpoint_IsEnabled(t *testing.T) {
|
||||||
if !(&Endpoint{Enabled: nil}).IsEnabled() {
|
if !(&Endpoint{Enabled: nil}).IsEnabled() {
|
||||||
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
t.Error("endpoint.IsEnabled() should've returned true, because Enabled was set to nil")
|
||||||
@@ -697,6 +746,76 @@ func TestEndpoint_buildHTTPRequestWithHostHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_buildHTTPRequestWithLowercaseUserAgent(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 200")
|
||||||
|
endpoint := Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Conditions: []Condition{condition},
|
||||||
|
Headers: map[string]string{
|
||||||
|
"user-agent": "CustomAgent/1.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
|
if _, exists := endpoint.Headers[UserAgentHeader]; exists {
|
||||||
|
t.Error("User-Agent header should not have been added since user-agent was already specified")
|
||||||
|
}
|
||||||
|
request := endpoint.buildHTTPRequest()
|
||||||
|
if userAgent := request.Header.Get("User-Agent"); userAgent != "CustomAgent/1.0" {
|
||||||
|
t.Errorf("request.Header.Get(User-Agent) should've been CustomAgent/1.0, but was %s", userAgent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_buildHTTPRequestWithLowercaseContentType(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 200")
|
||||||
|
endpoint := Endpoint{
|
||||||
|
Name: "website-graphql",
|
||||||
|
URL: "https://twin.sh/graphql",
|
||||||
|
Method: "POST",
|
||||||
|
Conditions: []Condition{condition},
|
||||||
|
GraphQL: true,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"content-type": "application/graphql",
|
||||||
|
},
|
||||||
|
Body: `{ users { id } }`,
|
||||||
|
}
|
||||||
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
|
if _, exists := endpoint.Headers[ContentTypeHeader]; exists {
|
||||||
|
t.Error("Content-Type header should not have been added since content-type was already specified")
|
||||||
|
}
|
||||||
|
request := endpoint.buildHTTPRequest()
|
||||||
|
if contentType := request.Header.Get("Content-Type"); contentType != "application/graphql" {
|
||||||
|
t.Errorf("request.Header.Get(Content-Type) should've been application/graphql, but was %s", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndpoint_buildHTTPRequestWithLowercaseHostHeader(t *testing.T) {
|
||||||
|
condition := Condition("[STATUS] == 200")
|
||||||
|
endpoint := Endpoint{
|
||||||
|
Name: "website-health",
|
||||||
|
URL: "https://twin.sh/health",
|
||||||
|
Method: "POST",
|
||||||
|
Conditions: []Condition{condition},
|
||||||
|
Headers: map[string]string{
|
||||||
|
"host": "example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := endpoint.ValidateAndSetDefaults()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("did not expect an error, got", err)
|
||||||
|
}
|
||||||
|
request := endpoint.buildHTTPRequest()
|
||||||
|
if request.Host != "example.com" {
|
||||||
|
t.Error("request.Host should've been example.com, but was", request.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEndpoint_buildHTTPRequestWithGraphQLEnabled(t *testing.T) {
|
func TestEndpoint_buildHTTPRequestWithGraphQLEnabled(t *testing.T) {
|
||||||
condition := Condition("[STATUS] == 200")
|
condition := Condition("[STATUS] == 200")
|
||||||
endpoint := Endpoint{
|
endpoint := Endpoint{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,11 +67,7 @@ type Result struct {
|
|||||||
// AddError adds an error to the result's list of errors.
|
// AddError adds an error to the result's list of errors.
|
||||||
// It also ensures that there are no duplicates.
|
// It also ensures that there are no duplicates.
|
||||||
func (r *Result) AddError(error string) {
|
func (r *Result) AddError(error string) {
|
||||||
for _, resultError := range r.Errors {
|
if !slices.Contains(r.Errors, error) {
|
||||||
if resultError == error {
|
r.Errors = append(r.Errors, error+"")
|
||||||
// If the error already exists, don't add it
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
r.Errors = append(r.Errors, error+"")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ type Config struct {
|
|||||||
// DontResolveFailedConditions whether to resolve failed conditions in the Result for display in the UI
|
// DontResolveFailedConditions whether to resolve failed conditions in the Result for display in the UI
|
||||||
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
DontResolveFailedConditions bool `yaml:"dont-resolve-failed-conditions"`
|
||||||
|
|
||||||
|
// ResolveSuccessfulConditions whether to resolve successful conditions in the Result for display in the UI
|
||||||
|
ResolveSuccessfulConditions bool `yaml:"resolve-successful-conditions"`
|
||||||
|
|
||||||
// Badge is the configuration for the badges generated
|
// Badge is the configuration for the badges generated
|
||||||
Badge *Badge `yaml:"badge"`
|
Badge *Badge `yaml:"badge"`
|
||||||
}
|
}
|
||||||
@@ -63,6 +66,7 @@ func GetDefaultConfig() *Config {
|
|||||||
HidePort: false,
|
HidePort: false,
|
||||||
HideErrors: false,
|
HideErrors: false,
|
||||||
DontResolveFailedConditions: false,
|
DontResolveFailedConditions: false,
|
||||||
|
ResolveSuccessfulConditions: false,
|
||||||
HideConditions: false,
|
HideConditions: false,
|
||||||
Badge: &Badge{
|
Badge: &Badge{
|
||||||
ResponseTime: &ResponseTime{
|
ResponseTime: &ResponseTime{
|
||||||
|
|||||||
@@ -340,9 +340,9 @@ func TestGontext_ConcurrentAccess(t *testing.T) {
|
|||||||
done := make(chan bool, 10)
|
done := make(chan bool, 10)
|
||||||
|
|
||||||
// Start 5 goroutines that read values
|
// Start 5 goroutines that read values
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
for j := 0; j < 100; j++ {
|
for range 100 {
|
||||||
_, err := ctx.Get("counter")
|
_, err := ctx.Get("counter")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Reader %d error: %v", id, err)
|
t.Errorf("Reader %d error: %v", id, err)
|
||||||
@@ -353,9 +353,9 @@ func TestGontext_ConcurrentAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start 5 goroutines that write values
|
// Start 5 goroutines that write values
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
go func(id int) {
|
go func(id int) {
|
||||||
for j := 0; j < 100; j++ {
|
for j := range 100 {
|
||||||
err := ctx.Set("counter", id*1000+j)
|
err := ctx.Set("counter", id*1000+j)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Writer %d error: %v", id, err)
|
t.Errorf("Writer %d error: %v", id, err)
|
||||||
@@ -366,7 +366,7 @@ func TestGontext_ConcurrentAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all goroutines to complete
|
// Wait for all goroutines to complete
|
||||||
for i := 0; i < 10; i++ {
|
for range 10 {
|
||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package maintenance
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -70,13 +71,7 @@ func (c *Config) ValidateAndSetDefaults() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, day := range c.Every {
|
for _, day := range c.Every {
|
||||||
isDayValid := false
|
isDayValid := slices.Contains(longDayNames, day)
|
||||||
for _, longDayName := range longDayNames {
|
|
||||||
if day == longDayName {
|
|
||||||
isDayValid = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isDayValid {
|
if !isDayValid {
|
||||||
return errInvalidDayName
|
return errInvalidDayName
|
||||||
}
|
}
|
||||||
@@ -118,7 +113,7 @@ func (c *Config) IsUnderMaintenance() bool {
|
|||||||
// Set to midnight prior to adding duration
|
// Set to midnight prior to adding duration
|
||||||
dayWhereMaintenancePeriodWouldStart := time.Date(now.Year(), now.Month(), adjustedDate, 0, 0, 0, 0, now.Location())
|
dayWhereMaintenancePeriodWouldStart := time.Date(now.Year(), now.Month(), adjustedDate, 0, 0, 0, 0, now.Location())
|
||||||
hasMaintenanceEveryDay := len(c.Every) == 0
|
hasMaintenanceEveryDay := len(c.Every) == 0
|
||||||
hasMaintenancePeriodScheduledToStartOnThatWeekday := c.hasDay(dayWhereMaintenancePeriodWouldStart.Weekday().String())
|
hasMaintenancePeriodScheduledToStartOnThatWeekday := slices.Contains(c.Every, dayWhereMaintenancePeriodWouldStart.Weekday().String())
|
||||||
if !hasMaintenanceEveryDay && !hasMaintenancePeriodScheduledToStartOnThatWeekday {
|
if !hasMaintenanceEveryDay && !hasMaintenancePeriodScheduledToStartOnThatWeekday {
|
||||||
// The day when the maintenance period would start is not scheduled
|
// The day when the maintenance period would start is not scheduled
|
||||||
// to have any maintenance, so we can just return false.
|
// to have any maintenance, so we can just return false.
|
||||||
@@ -129,15 +124,6 @@ func (c *Config) IsUnderMaintenance() bool {
|
|||||||
return now.After(startOfMaintenancePeriod) && now.Before(endOfMaintenancePeriod)
|
return now.After(startOfMaintenancePeriod) && now.Before(endOfMaintenancePeriod)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) hasDay(day string) bool {
|
|
||||||
for _, d := range c.Every {
|
|
||||||
if d == day {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hhmmToDuration(s string) (time.Duration, error) {
|
func hhmmToDuration(s string) (time.Duration, error) {
|
||||||
if len(s) != 5 {
|
if len(s) != 5 {
|
||||||
return 0, errInvalidMaintenanceStartFormat
|
return 0, errInvalidMaintenanceStartFormat
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func (t *SSHTunnel) Dial(network, addr string) (net.Conn, error) {
|
|||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
const baseDelay = 500 * time.Millisecond
|
const baseDelay = 500 * time.Millisecond
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
for attempt := range maxRetries {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
// Exponential backoff: 500ms, 1s, 2s
|
// Exponential backoff: 500ms, 1s, 2s
|
||||||
delay := baseDelay << (attempt - 1)
|
delay := baseDelay << (attempt - 1)
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ const (
|
|||||||
defaultDashboardSubheading = "Monitor the health of your endpoints in real-time"
|
defaultDashboardSubheading = "Monitor the health of your endpoints in real-time"
|
||||||
defaultLogo = ""
|
defaultLogo = ""
|
||||||
defaultLink = ""
|
defaultLink = ""
|
||||||
|
defaultFavicon = "/favicon.ico"
|
||||||
|
defaultFavicon16 = "/favicon-16x16.png"
|
||||||
|
defaultFavicon32 = "/favicon-32x32.png"
|
||||||
defaultCustomCSS = ""
|
defaultCustomCSS = ""
|
||||||
defaultSortBy = "name"
|
defaultSortBy = "name"
|
||||||
defaultFilterBy = "none"
|
defaultFilterBy = "none"
|
||||||
|
defaultLoginSubtitle = "System Monitoring Dashboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -39,11 +43,13 @@ type Config struct {
|
|||||||
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
|
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
|
||||||
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
|
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
|
||||||
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
|
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
|
||||||
|
Favicon Favicon `yaml:"favicon,omitempty"` // Favourite icon to display in web browser tab or address bar
|
||||||
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
|
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
|
||||||
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
|
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
|
||||||
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
|
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
|
||||||
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
|
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
|
||||||
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
|
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
|
||||||
|
LoginSubtitle string `yaml:"login-subtitle,omitempty"` // LoginSubtitle is the subtitle displayed on the OIDC login page
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
// Non-configurable - used for UI rendering //
|
// Non-configurable - used for UI rendering //
|
||||||
//////////////////////////////////////////////
|
//////////////////////////////////////////////
|
||||||
@@ -71,6 +77,12 @@ func (btn *Button) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Favicon struct {
|
||||||
|
Default string `yaml:"default,omitempty"` // URL or path to default favourite icon.
|
||||||
|
Size16x16 string `yaml:"size16x16,omitempty"` // URL or path to favourite icon for 16x16 size.
|
||||||
|
Size32x32 string `yaml:"size32x32,omitempty"` // URL or path to favourite icon for 32x32 size.
|
||||||
|
}
|
||||||
|
|
||||||
// GetDefaultConfig returns a Config struct with the default values
|
// GetDefaultConfig returns a Config struct with the default values
|
||||||
func GetDefaultConfig() *Config {
|
func GetDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
@@ -85,7 +97,13 @@ func GetDefaultConfig() *Config {
|
|||||||
DarkMode: &defaultDarkMode,
|
DarkMode: &defaultDarkMode,
|
||||||
DefaultSortBy: defaultSortBy,
|
DefaultSortBy: defaultSortBy,
|
||||||
DefaultFilterBy: defaultFilterBy,
|
DefaultFilterBy: defaultFilterBy,
|
||||||
|
LoginSubtitle: defaultLoginSubtitle,
|
||||||
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
|
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
|
||||||
|
Favicon: Favicon{
|
||||||
|
Default: defaultFavicon,
|
||||||
|
Size16x16: defaultFavicon16,
|
||||||
|
Size32x32: defaultFavicon32,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +146,18 @@ func (cfg *Config) ValidateAndSetDefaults() error {
|
|||||||
} else if cfg.DefaultFilterBy != "none" && cfg.DefaultFilterBy != "failing" && cfg.DefaultFilterBy != "unstable" {
|
} else if cfg.DefaultFilterBy != "none" && cfg.DefaultFilterBy != "failing" && cfg.DefaultFilterBy != "unstable" {
|
||||||
return ErrInvalidDefaultFilterBy
|
return ErrInvalidDefaultFilterBy
|
||||||
}
|
}
|
||||||
|
if len(cfg.LoginSubtitle) == 0 {
|
||||||
|
cfg.LoginSubtitle = defaultLoginSubtitle
|
||||||
|
}
|
||||||
|
if len(cfg.Favicon.Default) == 0 {
|
||||||
|
cfg.Favicon.Default = defaultFavicon
|
||||||
|
}
|
||||||
|
if len(cfg.Favicon.Size16x16) == 0 {
|
||||||
|
cfg.Favicon.Size16x16 = defaultFavicon16
|
||||||
|
}
|
||||||
|
if len(cfg.Favicon.Size32x32) == 0 {
|
||||||
|
cfg.Favicon.Size32x32 = defaultFavicon32
|
||||||
|
}
|
||||||
for _, btn := range cfg.Buttons {
|
for _, btn := range cfg.Buttons {
|
||||||
if err := btn.Validate(); err != nil {
|
if err := btn.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
if cfg.DefaultFilterBy != defaultFilterBy {
|
if cfg.DefaultFilterBy != defaultFilterBy {
|
||||||
t.Errorf("expected defaultFilterBy to be %s, got %s", defaultFilterBy, cfg.DefaultFilterBy)
|
t.Errorf("expected defaultFilterBy to be %s, got %s", defaultFilterBy, cfg.DefaultFilterBy)
|
||||||
}
|
}
|
||||||
|
if cfg.Favicon.Default != defaultFavicon {
|
||||||
|
t.Errorf("expected favicon to be %s, got %s", defaultFavicon, cfg.Favicon.Default)
|
||||||
|
}
|
||||||
|
if cfg.Favicon.Size16x16 != defaultFavicon16 {
|
||||||
|
t.Errorf("expected favicon to be %s, got %s", defaultFavicon16, cfg.Favicon.Size16x16)
|
||||||
|
}
|
||||||
|
if cfg.Favicon.Size32x32 != defaultFavicon32 {
|
||||||
|
t.Errorf("expected favicon to be %s, got %s", defaultFavicon32, cfg.Favicon.Size32x32)
|
||||||
|
}
|
||||||
|
if cfg.LoginSubtitle != defaultLoginSubtitle {
|
||||||
|
t.Errorf("expected LoginSubtitle to be %s, got %s", defaultLoginSubtitle, cfg.LoginSubtitle)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
t.Run("custom-values", func(t *testing.T) {
|
t.Run("custom-values", func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -53,6 +65,7 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
Link: "https://example.com",
|
Link: "https://example.com",
|
||||||
DefaultSortBy: "health",
|
DefaultSortBy: "health",
|
||||||
DefaultFilterBy: "failing",
|
DefaultFilterBy: "failing",
|
||||||
|
LoginSubtitle: "Welcome",
|
||||||
}
|
}
|
||||||
if err := cfg.ValidateAndSetDefaults(); err != nil {
|
if err := cfg.ValidateAndSetDefaults(); err != nil {
|
||||||
t.Error("expected no error, got", err.Error())
|
t.Error("expected no error, got", err.Error())
|
||||||
@@ -84,6 +97,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
if cfg.DefaultFilterBy != "failing" {
|
if cfg.DefaultFilterBy != "failing" {
|
||||||
t.Errorf("expected defaultFilterBy to be preserved, got %s", cfg.DefaultFilterBy)
|
t.Errorf("expected defaultFilterBy to be preserved, got %s", cfg.DefaultFilterBy)
|
||||||
}
|
}
|
||||||
|
if cfg.LoginSubtitle != "Welcome" {
|
||||||
|
t.Errorf("expected LoginSubtitle to be preserved, got %s", cfg.LoginSubtitle)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
t.Run("partial-custom-values", func(t *testing.T) {
|
t.Run("partial-custom-values", func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -110,6 +126,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
|||||||
if cfg.Description != defaultDescription {
|
if cfg.Description != defaultDescription {
|
||||||
t.Errorf("expected description to use default, got %s", cfg.Description)
|
t.Errorf("expected description to use default, got %s", cfg.Description)
|
||||||
}
|
}
|
||||||
|
if cfg.LoginSubtitle != defaultLoginSubtitle {
|
||||||
|
t.Errorf("expected LoginSubtitle to use default, got %s", cfg.LoginSubtitle)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +191,9 @@ func TestGetDefaultConfig(t *testing.T) {
|
|||||||
if defaultConfig.DefaultFilterBy != defaultFilterBy {
|
if defaultConfig.DefaultFilterBy != defaultFilterBy {
|
||||||
t.Error("expected GetDefaultConfig() to return defaultFilterBy, got", defaultConfig.DefaultFilterBy)
|
t.Error("expected GetDefaultConfig() to return defaultFilterBy, got", defaultConfig.DefaultFilterBy)
|
||||||
}
|
}
|
||||||
|
if defaultConfig.LoginSubtitle != defaultLoginSubtitle {
|
||||||
|
t.Error("expected GetDefaultConfig() to return defaultLoginSubtitle, got", defaultConfig.LoginSubtitle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_ValidateAndSetDefaults_DefaultSortBy(t *testing.T) {
|
func TestConfig_ValidateAndSetDefaults_DefaultSortBy(t *testing.T) {
|
||||||
|
|||||||
@@ -4,13 +4,3 @@ package config
|
|||||||
func toPtr[T any](value T) *T {
|
func toPtr[T any](value T) *T {
|
||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
// contains checks if a key exists in the slice
|
|
||||||
func contains[T comparable](slice []T, key T) bool {
|
|
||||||
for _, item := range slice {
|
|
||||||
if item == key {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,106 +1,103 @@
|
|||||||
module github.com/TwiN/gatus/v5
|
module github.com/TwiN/gatus/v5
|
||||||
|
|
||||||
go 1.24.4
|
go 1.26.3
|
||||||
|
|
||||||
toolchain go1.24.7
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.22.1
|
code.gitea.io/sdk/gitea v0.25.1
|
||||||
github.com/TwiN/deepmerge v0.2.2
|
github.com/TwiN/deepmerge v0.2.2
|
||||||
github.com/TwiN/g8/v2 v2.0.0
|
github.com/TwiN/g8/v2 v2.0.0
|
||||||
github.com/TwiN/gocache/v2 v2.4.0
|
github.com/TwiN/gocache/v2 v2.4.0
|
||||||
github.com/TwiN/health v1.6.0
|
github.com/TwiN/health v1.6.0
|
||||||
github.com/TwiN/logr v0.3.1
|
github.com/TwiN/logr v0.3.1
|
||||||
github.com/TwiN/whois v1.2.0
|
github.com/TwiN/whois v1.3.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0
|
github.com/aws/aws-sdk-go-v2 v1.41.7
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
github.com/aws/aws-sdk-go-v2/config v1.32.17
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.16
|
||||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.7
|
github.com/aws/aws-sdk-go-v2/service/ses v1.34.24
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0
|
github.com/coreos/go-oidc/v3 v3.18.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.9
|
github.com/gofiber/fiber/v2 v2.52.13
|
||||||
github.com/google/go-github/v48 v48.2.0
|
github.com/google/go-github/v48 v48.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2
|
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.12.3
|
||||||
github.com/miekg/dns v1.1.68
|
github.com/miekg/dns v1.1.72
|
||||||
github.com/prometheus-community/pro-bing v0.7.0
|
github.com/prometheus-community/pro-bing v0.8.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/registrobr/rdap v1.1.8
|
github.com/registrobr/rdap v1.1.8
|
||||||
github.com/valyala/fasthttp v1.67.0
|
github.com/valyala/fasthttp v1.71.0
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.2
|
github.com/wcharczuk/go-chart/v2 v2.1.2
|
||||||
golang.org/x/crypto v0.45.0
|
golang.org/x/crypto v0.52.0
|
||||||
golang.org/x/oauth2 v0.32.0
|
golang.org/x/oauth2 v0.36.0
|
||||||
golang.org/x/sync v0.18.0
|
golang.org/x/sync v0.20.0
|
||||||
google.golang.org/api v0.252.0
|
google.golang.org/api v0.280.0
|
||||||
google.golang.org/grpc v1.77.0
|
google.golang.org/grpc v1.81.1
|
||||||
gopkg.in/mail.v2 v2.3.1
|
gopkg.in/mail.v2 v2.3.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.39.1
|
modernc.org/sqlite v1.50.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.17.0 // indirect
|
cloud.google.com/go/auth v0.20.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
github.com/42wim/httpsig v1.2.3 // indirect
|
github.com/42wim/httpsig v1.2.4 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
|
||||||
github.com/aws/smithy-go v1.23.2 // indirect
|
github.com/aws/smithy-go v1.25.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/image v0.40.0 // indirect
|
||||||
golang.org/x/image v0.18.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/net v0.54.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
modernc.org/libc v1.66.10 // indirect
|
modernc.org/libc v1.72.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
code.gitea.io/sdk/gitea v0.25.1 h1:yywxWwoV+SdjHtbC6unBiXojWdZOtoHuGhEazEXeWuE=
|
||||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
code.gitea.io/sdk/gitea v0.25.1/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg=
|
||||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
||||||
github.com/TwiN/deepmerge v0.2.2 h1:FUG9QMIYg/j2aQyPPhA3XTFJwXSNHI/swaR4Lbyxwg4=
|
github.com/TwiN/deepmerge v0.2.2 h1:FUG9QMIYg/j2aQyPPhA3XTFJwXSNHI/swaR4Lbyxwg4=
|
||||||
github.com/TwiN/deepmerge v0.2.2/go.mod h1:4OHvjV3pPNJCJZBHswYAwk6rxiD8h8YZ+9cPo7nu4oI=
|
github.com/TwiN/deepmerge v0.2.2/go.mod h1:4OHvjV3pPNJCJZBHswYAwk6rxiD8h8YZ+9cPo7nu4oI=
|
||||||
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
|
github.com/TwiN/g8/v2 v2.0.0 h1:+hwIbRLMhDd2iwHzkZUPp2FkX7yTx8ddYOnS91HkDqQ=
|
||||||
@@ -18,48 +18,50 @@ 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/health v1.6.0/go.mod h1:Z6TszwQPMvtSiVx1QMidVRgvVr4KZGfiwqcD7/Z+3iw=
|
||||||
github.com/TwiN/logr v0.3.1 h1:CfTKA83jUmsAoxqrr3p4JxEkqXOBnEE9/f35L5MODy4=
|
github.com/TwiN/logr v0.3.1 h1:CfTKA83jUmsAoxqrr3p4JxEkqXOBnEE9/f35L5MODy4=
|
||||||
github.com/TwiN/logr v0.3.1/go.mod h1:BZgZFYq6fQdU3KtR8qYato3zUEw53yQDaIuujHb55Jw=
|
github.com/TwiN/logr v0.3.1/go.mod h1:BZgZFYq6fQdU3KtR8qYato3zUEw53yQDaIuujHb55Jw=
|
||||||
github.com/TwiN/whois v1.2.0 h1:/Z22SrS3Z0FQgMl1+4bKSu9UmEQTfGx9i9J4Hn18eQk=
|
github.com/TwiN/whois v1.3.0 h1:V2+IUh5OGim8F3axTOMSlVmxNRaBrgpyiv5U0Dvbt5I=
|
||||||
github.com/TwiN/whois v1.2.0/go.mod h1:TjipCMpJRAJYKmtz/rXQBU6UGxMh6bk8SHazu7OMnQE=
|
github.com/TwiN/whois v1.3.0/go.mod h1:TjipCMpJRAJYKmtz/rXQBU6UGxMh6bk8SHazu7OMnQE=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.7 h1:As+tSgow5R48+hFwkgtEBOFYFtvSEqMal2QSMRccWgw=
|
github.com/aws/aws-sdk-go-v2/service/ses v1.34.24 h1:ZPCoU087iv2IWJKNQyN+MYOqZAM9lEVevS9MHWGG4wI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ses v1.34.7/go.mod h1:DgSdE3UxodNau1977aGbt4YixHYESTNAEStFo9qJ2TQ=
|
github.com/aws/aws-sdk-go-v2/service/ses v1.34.24/go.mod h1:4T8OWyQ9nkdUrcqHHJkyibP5TfcWDTe4kWL40tiCOcs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
|
||||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||||
|
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||||
@@ -70,93 +72,90 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
|
||||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
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/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-github/v48 v48.2.0 h1:68puzySE6WqUY9KWmpOsDEQfDZsso98rT6pZcz9HqcE=
|
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-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.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 h1:36qep4gxKs+JgeHGWeQ040RyZdt9kQlLglL1rFVn/oQ=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
||||||
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
github.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc=
|
||||||
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
github.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0=
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/registrobr/rdap v1.1.8 h1:7egYAM8MsuencdP9mvF/892f8OjXvUFSyp5cT1Lg45U=
|
github.com/registrobr/rdap v1.1.8 h1:7egYAM8MsuencdP9mvF/892f8OjXvUFSyp5cT1Lg45U=
|
||||||
github.com/registrobr/rdap v1.1.8/go.mod h1:VY2DVrpsJpUfy9gj2QvurGymCgZV11/11cxQz5CxO+w=
|
github.com/registrobr/rdap v1.1.8/go.mod h1:VY2DVrpsJpUfy9gj2QvurGymCgZV11/11cxQz5CxO+w=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
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/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.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
|
github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
|
||||||
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
|
github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
@@ -164,24 +163,24 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
@@ -189,19 +188,18 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
|
||||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
|
||||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||||
|
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||||
|
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.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/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -211,33 +209,31 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20220722155255-886fb9371eb4/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220520151302-bc2c85ada10a/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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -246,8 +242,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
@@ -257,30 +253,33 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk=
|
||||||
google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI=
|
google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU=
|
||||||
google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw=
|
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -290,28 +289,30 @@ gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
|||||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4=
|
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||||
modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func extractValue(currentKey string, value interface{}) interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
arrayIndex, err := strconv.Atoi(index)
|
arrayIndex, err := strconv.Atoi(index)
|
||||||
if err != nil {
|
if err != nil || arrayIndex < 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
currentKeyWithoutIndex := currentKey[:startOfBracket]
|
currentKeyWithoutIndex := currentKey[:startOfBracket]
|
||||||
|
|||||||
@@ -174,6 +174,38 @@ func TestEval(t *testing.T) {
|
|||||||
ExpectedOutputLength: 18,
|
ExpectedOutputLength: 18,
|
||||||
ExpectedError: false,
|
ExpectedError: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "negative-index-on-keyed-array",
|
||||||
|
Path: "data[-1]",
|
||||||
|
Data: `{"data": [1, 2, 3]}`,
|
||||||
|
ExpectedOutput: "",
|
||||||
|
ExpectedOutputLength: 0,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "negative-index-on-root-array",
|
||||||
|
Path: "[-1]",
|
||||||
|
Data: `[1, 2, 3]`,
|
||||||
|
ExpectedOutput: "",
|
||||||
|
ExpectedOutputLength: 0,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "negative-index-followed-by-key",
|
||||||
|
Path: "data[-1].name",
|
||||||
|
Data: `{"data": [{"name": "value"}]}`,
|
||||||
|
ExpectedOutput: "",
|
||||||
|
ExpectedOutputLength: 0,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "negative-index-nested-array",
|
||||||
|
Path: "data[0][-1]",
|
||||||
|
Data: `{"data": [[1, 2, 3]]}`,
|
||||||
|
ExpectedOutput: "",
|
||||||
|
ExpectedOutputLength: 0,
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ type Config struct {
|
|||||||
|
|
||||||
// ValidateAndSetDefaults returns whether the security configuration is valid or not and sets default values.
|
// ValidateAndSetDefaults returns whether the security configuration is valid or not and sets default values.
|
||||||
func (c *Config) ValidateAndSetDefaults() bool {
|
func (c *Config) ValidateAndSetDefaults() bool {
|
||||||
return (c.Basic != nil && c.Basic.isValid()) || (c.OIDC != nil && c.OIDC.ValidateAndSetDefaults())
|
return (c.Basic == nil || c.Basic.isValid()) && (c.OIDC == nil || c.OIDC.ValidateAndSetDefaults())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterHandlers registers all handlers required based on the security configuration
|
// RegisterHandlers registers all handlers required based on the security configuration
|
||||||
|
|||||||
+75
-5
@@ -10,12 +10,82 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
func TestConfig_ValidateAndSetDefaults(t *testing.T) {
|
||||||
c := &Config{
|
validBasicConfig := &BasicConfig{
|
||||||
Basic: nil,
|
Username: "test",
|
||||||
OIDC: nil,
|
PasswordBcryptHashBase64Encoded: "somevalue",
|
||||||
}
|
}
|
||||||
if c.ValidateAndSetDefaults() {
|
validOIDCConfig := &OIDCConfig{
|
||||||
t.Error("expected empty config to be valid")
|
IssuerURL: "testurl",
|
||||||
|
RedirectURL: "testredirecturl/authorization-code/callback",
|
||||||
|
ClientID: "testid",
|
||||||
|
ClientSecret: "testsecret",
|
||||||
|
Scopes: []string{"testscope"},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scenario struct {
|
||||||
|
Name string
|
||||||
|
Config *Config
|
||||||
|
ExpectValid bool
|
||||||
|
}
|
||||||
|
scenarios := []Scenario{
|
||||||
|
{
|
||||||
|
Name: "empty",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: nil,
|
||||||
|
OIDC: nil,
|
||||||
|
},
|
||||||
|
ExpectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "empty-basic",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: &BasicConfig{},
|
||||||
|
OIDC: nil,
|
||||||
|
},
|
||||||
|
ExpectValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "empty-oidc",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: nil,
|
||||||
|
OIDC: &OIDCConfig{},
|
||||||
|
},
|
||||||
|
ExpectValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "valid-basic-only",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: validBasicConfig,
|
||||||
|
OIDC: nil,
|
||||||
|
},
|
||||||
|
ExpectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "valid-oidc-only",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: nil,
|
||||||
|
OIDC: validOIDCConfig,
|
||||||
|
},
|
||||||
|
ExpectValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "valid-basic-and-oidc",
|
||||||
|
Config: &Config{
|
||||||
|
Basic: validBasicConfig,
|
||||||
|
OIDC: validOIDCConfig,
|
||||||
|
},
|
||||||
|
ExpectValid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
isValid := scenario.Config.ValidateAndSetDefaults()
|
||||||
|
if isValid && !scenario.ExpectValid {
|
||||||
|
t.Errorf("scenario %s: expected config to be invalid", scenario.Name)
|
||||||
|
} else if !isValid && scenario.ExpectValid {
|
||||||
|
t.Errorf("scenario %s: expected config to be valid", scenario.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package memory
|
package memory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -237,13 +238,7 @@ func (s *Store) InsertSuiteResult(su *suite.Suite, result *suite.Result) error {
|
|||||||
func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int {
|
func (s *Store) DeleteAllEndpointStatusesNotInKeys(keys []string) int {
|
||||||
var keysToDelete []string
|
var keysToDelete []string
|
||||||
for _, existingKey := range s.endpointCache.GetKeysByPattern("*", 0) {
|
for _, existingKey := range s.endpointCache.GetKeysByPattern("*", 0) {
|
||||||
shouldDelete := true
|
shouldDelete := !slices.Contains(keys, existingKey)
|
||||||
for _, k := range keys {
|
|
||||||
if existingKey == k {
|
|
||||||
shouldDelete = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if shouldDelete {
|
if shouldDelete {
|
||||||
keysToDelete = append(keysToDelete, existingKey)
|
keysToDelete = append(keysToDelete, existingKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ func TestSuiteResultOrdering(t *testing.T) {
|
|||||||
baseTime := time.Now().Add(-5 * time.Hour)
|
baseTime := time.Now().Add(-5 * time.Hour)
|
||||||
timestamps := make([]time.Time, 5)
|
timestamps := make([]time.Time, 5)
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
timestamp := baseTime.Add(time.Duration(i) * time.Hour)
|
timestamp := baseTime.Add(time.Duration(i) * time.Hour)
|
||||||
timestamps[i] = timestamp
|
timestamps[i] = timestamp
|
||||||
result := &suite.Result{
|
result := &suite.Result{
|
||||||
@@ -878,7 +878,7 @@ func TestSuiteResultOrdering(t *testing.T) {
|
|||||||
smallSuite := &suite.Suite{Name: "small-suite", Group: "test"}
|
smallSuite := &suite.Suite{Name: "small-suite", Group: "test"}
|
||||||
|
|
||||||
// Insert 6 results, should keep only the newest 3
|
// Insert 6 results, should keep only the newest 3
|
||||||
for i := 0; i < 6; i++ {
|
for i := range 6 {
|
||||||
result := &suite.Result{
|
result := &suite.Result{
|
||||||
Name: smallSuite.Name,
|
Name: smallSuite.Name,
|
||||||
Group: smallSuite.Group,
|
Group: smallSuite.Group,
|
||||||
@@ -925,7 +925,7 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
|
|
||||||
// Create endpoints for concurrent testing
|
// Create endpoints for concurrent testing
|
||||||
endpoints := make([]*endpoint.Endpoint, numGoroutines)
|
endpoints := make([]*endpoint.Endpoint, numGoroutines)
|
||||||
for i := 0; i < numGoroutines; i++ {
|
for i := range numGoroutines {
|
||||||
endpoints[i] = &endpoint.Endpoint{
|
endpoints[i] = &endpoint.Endpoint{
|
||||||
Name: "endpoint-" + string(rune('A'+i)),
|
Name: "endpoint-" + string(rune('A'+i)),
|
||||||
Group: "concurrent",
|
Group: "concurrent",
|
||||||
@@ -934,12 +934,12 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Concurrently insert results for different endpoints
|
// Concurrently insert results for different endpoints
|
||||||
for i := 0; i < numGoroutines; i++ {
|
for i := range numGoroutines {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(endpointIndex int) {
|
go func(endpointIndex int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ep := endpoints[endpointIndex]
|
ep := endpoints[endpointIndex]
|
||||||
for j := 0; j < resultsPerGoroutine; j++ {
|
for j := range resultsPerGoroutine {
|
||||||
result := &endpoint.Result{
|
result := &endpoint.Result{
|
||||||
Success: j%2 == 0,
|
Success: j%2 == 0,
|
||||||
Timestamp: time.Now().Add(time.Duration(j) * time.Minute),
|
Timestamp: time.Now().Add(time.Duration(j) * time.Minute),
|
||||||
@@ -978,7 +978,7 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
|
|
||||||
// Create suites for concurrent testing
|
// Create suites for concurrent testing
|
||||||
suites := make([]*suite.Suite, numGoroutines)
|
suites := make([]*suite.Suite, numGoroutines)
|
||||||
for i := 0; i < numGoroutines; i++ {
|
for i := range numGoroutines {
|
||||||
suites[i] = &suite.Suite{
|
suites[i] = &suite.Suite{
|
||||||
Name: "suite-" + string(rune('A'+i)),
|
Name: "suite-" + string(rune('A'+i)),
|
||||||
Group: "concurrent",
|
Group: "concurrent",
|
||||||
@@ -986,12 +986,12 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Concurrently insert results for different suites
|
// Concurrently insert results for different suites
|
||||||
for i := 0; i < numGoroutines; i++ {
|
for i := range numGoroutines {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(suiteIndex int) {
|
go func(suiteIndex int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
su := suites[suiteIndex]
|
su := suites[suiteIndex]
|
||||||
for j := 0; j < resultsPerGoroutine; j++ {
|
for j := range resultsPerGoroutine {
|
||||||
result := &suite.Result{
|
result := &suite.Result{
|
||||||
Name: su.Name,
|
Name: su.Name,
|
||||||
Group: su.Group,
|
Group: su.Group,
|
||||||
@@ -1036,7 +1036,7 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
result := &endpoint.Result{
|
result := &endpoint.Result{
|
||||||
Success: true,
|
Success: true,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
@@ -1050,7 +1050,7 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for i := 0; i < 5; i++ {
|
for i := range 5 {
|
||||||
result := &suite.Result{
|
result := &suite.Result{
|
||||||
Name: testSuite.Name,
|
Name: testSuite.Name,
|
||||||
Group: testSuite.Group,
|
Group: testSuite.Group,
|
||||||
@@ -1066,7 +1066,7 @@ func TestStore_ConcurrentAccess(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for i := 0; i < 10; i++ {
|
for range 10 {
|
||||||
store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
store.GetAllEndpointStatuses(&paging.EndpointStatusParams{})
|
||||||
store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
store.GetAllSuiteStatuses(&paging.SuiteStatusParams{})
|
||||||
time.Sleep(1 * time.Millisecond)
|
time.Sleep(1 * time.Millisecond)
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import (
|
|||||||
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
|
||||||
ep := &testEndpoint
|
ep := &testEndpoint
|
||||||
status := endpoint.NewStatus(ep.Group, ep.Name)
|
status := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
for i := 0; i < storage.DefaultMaximumNumberOfResults; i++ {
|
for range storage.DefaultMaximumNumberOfResults {
|
||||||
AddResult(status, &testSuccessfulResult, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
AddResult(status, &testSuccessfulResult, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
||||||
}
|
}
|
||||||
for n := 0; n < b.N; n++ {
|
for b.Loop() {
|
||||||
ShallowCopyEndpointStatus(status, paging.NewEndpointStatusParams().WithResults(1, 20))
|
ShallowCopyEndpointStatus(status, paging.NewEndpointStatusParams().WithResults(1, 20))
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func TestAddResult(t *testing.T) {
|
func TestAddResult(t *testing.T) {
|
||||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
for i := 0; i < (storage.DefaultMaximumNumberOfResults+storage.DefaultMaximumNumberOfEvents)*2; i++ {
|
for i := range (storage.DefaultMaximumNumberOfResults + storage.DefaultMaximumNumberOfEvents) * 2 {
|
||||||
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
||||||
}
|
}
|
||||||
if len(endpointStatus.Results) != storage.DefaultMaximumNumberOfResults {
|
if len(endpointStatus.Results) != storage.DefaultMaximumNumberOfResults {
|
||||||
@@ -30,7 +30,7 @@ func TestShallowCopyEndpointStatus(t *testing.T) {
|
|||||||
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
|
||||||
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
|
||||||
ts := time.Now().Add(-25 * time.Hour)
|
ts := time.Now().Add(-25 * time.Hour)
|
||||||
for i := 0; i < 25; i++ {
|
for i := range 25 {
|
||||||
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
|
||||||
ts = ts.Add(time.Hour)
|
ts = ts.Add(time.Hour)
|
||||||
}
|
}
|
||||||
@@ -74,9 +74,9 @@ func TestShallowCopySuiteStatus(t *testing.T) {
|
|||||||
Key: testSuite.Key(),
|
Key: testSuite.Key(),
|
||||||
Results: []*suite.Result{},
|
Results: []*suite.Result{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := time.Now().Add(-25 * time.Hour)
|
ts := time.Now().Add(-25 * time.Hour)
|
||||||
for i := 0; i < 25; i++ {
|
for i := range 25 {
|
||||||
result := &suite.Result{
|
result := &suite.Result{
|
||||||
Name: testSuite.Name,
|
Name: testSuite.Name,
|
||||||
Group: testSuite.Group,
|
Group: testSuite.Group,
|
||||||
@@ -169,4 +169,3 @@ func TestShallowCopySuiteStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,5 +120,9 @@ func (s *Store) createPostgresSchema() error {
|
|||||||
_, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD COLUMN IF NOT EXISTS suite_result_id BIGINT REFERENCES suite_results(suite_result_id) ON DELETE CASCADE`)
|
_, _ = s.db.Exec(`ALTER TABLE endpoint_results ADD COLUMN IF NOT EXISTS suite_result_id BIGINT REFERENCES suite_results(suite_result_id) ON DELETE CASCADE`)
|
||||||
// Create index for suite_result_id
|
// Create index for suite_result_id
|
||||||
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS endpoint_results_suite_result_id_idx ON endpoint_results(suite_result_id)`)
|
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS endpoint_results_suite_result_id_idx ON endpoint_results(suite_result_id)`)
|
||||||
|
// Create index for endpoint_result_conditions
|
||||||
|
_, _ = s.db.Exec(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_endpoint_result_conditions_endpoint_result_id ON endpoint_result_conditions (endpoint_result_id)`)
|
||||||
|
// Create index for endpoint_results
|
||||||
|
_, _ = s.db.Exec(`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_endpoint_results_endpoint_id ON endpoint_results (endpoint_id)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-15
@@ -1176,23 +1176,26 @@ func (s *Store) GetAllSuiteStatuses(params *paging.SuiteStatusParams) ([]*suite.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
|
// Collect all suite statuses first before making nested queries.
|
||||||
|
// The lib/pq driver requires the previous query to be fully closed before starting
|
||||||
|
// new queries in the same transaction, otherwise it returns "pq: unexpected Parse
|
||||||
|
// response 'C'" errors. See: https://github.com/TwiN/gatus/issues/1435
|
||||||
var suiteStatuses []*suite.Status
|
var suiteStatuses []*suite.Status
|
||||||
|
suiteIDs := make(map[*suite.Status]int64) // Track suite_id for each status
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
status := &suite.Status{Results: []*suite.Result{}}
|
||||||
var suiteID int64
|
var suiteID int64
|
||||||
var key, name, group string
|
if err = rows.Scan(&suiteID, &status.Key, &status.Name, &status.Group); err != nil {
|
||||||
if err = rows.Scan(&suiteID, &key, &name, &group); err != nil {
|
rows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
suiteStatuses = append(suiteStatuses, status)
|
||||||
|
suiteIDs[status] = suiteID
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
status := &suite.Status{
|
for _, status := range suiteStatuses {
|
||||||
Name: name,
|
|
||||||
Group: group,
|
|
||||||
Key: key,
|
|
||||||
Results: []*suite.Result{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get suite results with pagination
|
// Get suite results with pagination
|
||||||
pageSize := 20
|
pageSize := 20
|
||||||
page := 1
|
page := 1
|
||||||
@@ -1205,17 +1208,16 @@ func (s *Store) GetAllSuiteStatuses(params *paging.SuiteStatusParams) ([]*suite.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suiteID := suiteIDs[status]
|
||||||
status.Results, err = s.getSuiteResults(tx, suiteID, page, pageSize)
|
status.Results, err = s.getSuiteResults(tx, suiteID, page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logr.Errorf("[sql.GetAllSuiteStatuses] Failed to retrieve results for suite_id=%d: %s", suiteID, err.Error())
|
logr.Errorf("[sql.GetAllSuiteStatuses] Failed to retrieve results for suite_id=%d: %s", suiteID, err.Error())
|
||||||
}
|
}
|
||||||
// Populate Name and Group fields on each result
|
// Populate Name and Group fields on each result
|
||||||
for _, result := range status.Results {
|
for _, result := range status.Results {
|
||||||
result.Name = name
|
result.Name = status.Name
|
||||||
result.Group = group
|
result.Group = status.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
suiteStatuses = append(suiteStatuses, status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = tx.Commit(); err != nil {
|
if err = tx.Commit(); err != nil {
|
||||||
@@ -1466,7 +1468,6 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
|
|||||||
logr.Errorf("[sql.getSuiteResults] Query failed: %v", err)
|
logr.Errorf("[sql.getSuiteResults] Query failed: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
type suiteResultData struct {
|
type suiteResultData struct {
|
||||||
result *suite.Result
|
result *suite.Result
|
||||||
id int64
|
id int64
|
||||||
@@ -1494,6 +1495,11 @@ func (s *Store) getSuiteResults(tx *sql.Tx, suiteID int64, page, pageSize int) (
|
|||||||
id: suiteResultID,
|
id: suiteResultID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// Close the rows before making nested queries to avoid PostgreSQL driver issues.
|
||||||
|
// The lib/pq driver requires the previous query to be fully closed before starting
|
||||||
|
// new queries in the same transaction, otherwise it returns "pq: unexpected Parse
|
||||||
|
// response 'C'" errors. See: https://github.com/TwiN/gatus/issues/1435
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
// Reverse the results to get chronological order (oldest to newest)
|
// Reverse the results to get chronological order (oldest to newest)
|
||||||
for i := len(resultsData)/2 - 1; i >= 0; i-- {
|
for i := len(resultsData)/2 - 1; i >= 0; i-- {
|
||||||
|
|||||||
@@ -900,7 +900,7 @@ func TestEventOrderingFix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Create many events over time
|
// Create many events over time
|
||||||
baseTime := time.Now().Add(-100 * time.Hour) // Start 100 hours ago
|
baseTime := time.Now().Add(-100 * time.Hour) // Start 100 hours ago
|
||||||
for i := 0; i < 50; i++ {
|
for i := range 50 {
|
||||||
result := &endpoint.Result{
|
result := &endpoint.Result{
|
||||||
Success: i%2 == 0, // Alternate between true/false to create events
|
Success: i%2 == 0, // Alternate between true/false to create events
|
||||||
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
Timestamp: baseTime.Add(time.Duration(i) * time.Hour),
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
|
|||||||
numberOfEndpoints := []int{10, 25, 50, 100}
|
numberOfEndpoints := []int{10, 25, 50, 100}
|
||||||
for _, numberOfEndpointsToCreate := range numberOfEndpoints {
|
for _, numberOfEndpointsToCreate := range numberOfEndpoints {
|
||||||
// Create endpoints and insert results
|
// Create endpoints and insert results
|
||||||
for i := 0; i < numberOfEndpointsToCreate; i++ {
|
for i := range numberOfEndpointsToCreate {
|
||||||
ep := testEndpoint
|
ep := testEndpoint
|
||||||
ep.Name = "endpoint" + strconv.Itoa(i)
|
ep.Name = "endpoint" + strconv.Itoa(i)
|
||||||
// InsertEndpointResult 20 results for each endpoint
|
// InsertEndpointResult 20 results for each endpoint
|
||||||
for j := 0; j < 20; j++ {
|
for range 20 {
|
||||||
scenario.Store.InsertEndpointResult(&ep, &testSuccessfulResult)
|
scenario.Store.InsertEndpointResult(&ep, &testSuccessfulResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ func BenchmarkStore_GetEndpointStatusByKey(b *testing.B) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
for i := 0; i < 50; i++ {
|
for range 50 {
|
||||||
scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult)
|
scenario.Store.InsertEndpointResult(&testEndpoint, &testSuccessfulResult)
|
||||||
scenario.Store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult)
|
scenario.Store.InsertEndpointResult(&testEndpoint, &testUnsuccessfulResult)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
|
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}", loginSubtitle: "{{ .UI.LoginSubtitle }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
|
||||||
// Initialize theme immediately to prevent flash
|
// Initialize theme immediately to prevent flash
|
||||||
(function() {
|
(function() {
|
||||||
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
|
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ .UI.Favicon.Size32x32 }}" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ .UI.Favicon.Size16x16 }}" />
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="{{ .UI.Favicon.Default }}" />
|
||||||
<link rel="stylesheet" href="/css/custom.css" />
|
<link rel="stylesheet" href="/css/custom.css" />
|
||||||
<meta name="description" content="{{ .UI.Description }}" />
|
<meta name="description" content="{{ .UI.Description }}" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|||||||
+12
-7
@@ -112,13 +112,14 @@
|
|||||||
<div v-else id="login-container" class="flex items-center justify-center min-h-screen p-4">
|
<div v-else id="login-container" class="flex items-center justify-center min-h-screen p-4">
|
||||||
<Card class="w-full max-w-md">
|
<Card class="w-full max-w-md">
|
||||||
<CardHeader class="text-center">
|
<CardHeader class="text-center">
|
||||||
<img
|
<div v-if="logo" class="flex items-center justify-center gap-4 mb-4">
|
||||||
src="./assets/logo.svg"
|
<img :src="logo" alt="" class="w-20 h-20 object-contain" />
|
||||||
alt="Gatus"
|
<div class="w-px h-12 bg-border"></div>
|
||||||
class="w-20 h-20 mx-auto mb-4"
|
<img src="./assets/logo.svg" alt="Gatus" class="w-20 h-20" />
|
||||||
/>
|
</div>
|
||||||
<CardTitle class="text-3xl">Gatus</CardTitle>
|
<img v-else src="./assets/logo.svg" alt="Gatus" class="w-20 h-20 mx-auto mb-4" />
|
||||||
<p class="text-muted-foreground mt-2">System Monitoring Dashboard</p>
|
<CardTitle class="text-3xl">{{ header }}</CardTitle>
|
||||||
|
<p class="text-muted-foreground mt-2">{{ loginSubtitle }}</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="route && route.query.error" class="mb-6">
|
<div v-if="route && route.query.error" class="mb-6">
|
||||||
@@ -191,6 +192,10 @@ const buttons = computed(() => {
|
|||||||
return window.config && window.config.buttons ? window.config.buttons : []
|
return window.config && window.config.buttons ? window.config.buttons : []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loginSubtitle = computed(() => {
|
||||||
|
return window.config && window.config.loginSubtitle && window.config.loginSubtitle !== '{{ .UI.LoginSubtitle }}' ? window.config.loginSubtitle : "System Monitoring Dashboard"
|
||||||
|
})
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="showAverageResponseTime = !showAverageResponseTime"
|
@click="toggleShowAverageResponseTime"
|
||||||
:title="showAverageResponseTime ? 'Show min-max response time' : 'Show average response time'"
|
:title="showAverageResponseTime ? 'Show min-max response time' : 'Show average response time'"
|
||||||
>
|
>
|
||||||
<Activity v-if="showAverageResponseTime" class="h-5 w-5" />
|
<Activity v-if="showAverageResponseTime" class="h-5 w-5" />
|
||||||
@@ -224,7 +224,7 @@ const events = ref([])
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const resultPageSize = 50
|
const resultPageSize = 50
|
||||||
const showResponseTimeChartAndBadges = ref(false)
|
const showResponseTimeChartAndBadges = ref(false)
|
||||||
const showAverageResponseTime = ref(false)
|
const showAverageResponseTime = ref(localStorage.getItem('gatus:show-average-response-time') !== 'false')
|
||||||
const selectedChartDuration = ref('24h')
|
const selectedChartDuration = ref('24h')
|
||||||
const isRefreshing = ref(false)
|
const isRefreshing = ref(false)
|
||||||
|
|
||||||
@@ -245,6 +245,11 @@ const hostname = computed(() => {
|
|||||||
return latestResult.value?.hostname || null
|
return latestResult.value?.hostname || null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toggleShowAverageResponseTime = () => {
|
||||||
|
showAverageResponseTime.value = !showAverageResponseTime.value
|
||||||
|
localStorage.setItem('gatus:show-average-response-time', showAverageResponseTime.value ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
|
||||||
const pageAverageResponseTime = computed(() => {
|
const pageAverageResponseTime = computed(() => {
|
||||||
// Use endpointStatus for current page's average response time
|
// Use endpointStatus for current page's average response time
|
||||||
if (!endpointStatus.value || !endpointStatus.value.results || endpointStatus.value.results.length === 0) {
|
if (!endpointStatus.value || !endpointStatus.value.results || endpointStatus.value.results.length === 0) {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ const itemsPerPage = 96
|
|||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const showOnlyFailing = ref(false)
|
const showOnlyFailing = ref(false)
|
||||||
const showRecentFailures = ref(false)
|
const showRecentFailures = ref(false)
|
||||||
const showAverageResponseTime = ref(true)
|
const showAverageResponseTime = ref(localStorage.getItem('gatus:show-average-response-time') !== 'false')
|
||||||
const groupByGroup = ref(false)
|
const groupByGroup = ref(false)
|
||||||
const sortBy = ref(localStorage.getItem('gatus:sort-by') || 'name')
|
const sortBy = ref(localStorage.getItem('gatus:sort-by') || 'name')
|
||||||
const uncollapsedGroups = ref(new Set())
|
const uncollapsedGroups = ref(new Set())
|
||||||
@@ -483,6 +483,7 @@ const goToPage = (page) => {
|
|||||||
|
|
||||||
const toggleShowAverageResponseTime = () => {
|
const toggleShowAverageResponseTime = () => {
|
||||||
showAverageResponseTime.value = !showAverageResponseTime.value
|
showAverageResponseTime.value = !showAverageResponseTime.value
|
||||||
|
localStorage.setItem('gatus:show-average-response-time', showAverageResponseTime.value ? 'true' : 'false')
|
||||||
}
|
}
|
||||||
|
|
||||||
const showTooltip = (result, event, action = 'hover') => {
|
const showTooltip = (result, event, action = 'hover') => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
|
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}", loginSubtitle: "{{ .UI.LoginSubtitle }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
|
||||||
// Initialize theme immediately to prevent flash
|
// Initialize theme immediately to prevent flash
|
||||||
(function() {
|
(function() {
|
||||||
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
|
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
|
||||||
@@ -8,4 +8,4 @@
|
|||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
}
|
}
|
||||||
})();</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
})();</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="{{ .UI.Favicon.Size32x32 }}"/><link rel="icon" type="image/png" sizes="16x16" href="{{ .UI.Favicon.Size16x16 }}"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="{{ .UI.Favicon.Default }}"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user