mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-14 14:27:51 +02:00
Compare commits
36 Commits
c1c1cd1bcb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8450b40e0c | ||
|
|
ac4436bea3 | ||
|
|
fa5cda83c2 | ||
|
|
35af36fbd2 | ||
|
|
e380ab6917 | ||
|
|
c3a432101b | ||
|
|
98e86b4c9c | ||
|
|
d40372842b | ||
|
|
c9b6279e61 | ||
|
|
9887b662ad | ||
|
|
71bc6f9b9f | ||
|
|
87468965dc | ||
|
|
db0da58ac8 | ||
|
|
b9b3a23063 | ||
|
|
ac34e58113 | ||
|
|
3c8703d1c0 | ||
|
|
01efba50a5 | ||
|
|
d0453f1ca6 | ||
|
|
7ffc6e81ce | ||
|
|
1aa9fcd31d | ||
|
|
bd52134558 | ||
|
|
3fac02f0c0 | ||
|
|
e4c0522cae | ||
|
|
f2ccaacecb | ||
|
|
a81ce14046 | ||
|
|
6e4a2c8a3d | ||
|
|
8cf39be6ef | ||
|
|
146ca4284c | ||
|
|
6eae195d9c | ||
|
|
30bf65991b | ||
|
|
f57f5883ce | ||
|
|
0472343730 | ||
|
|
d3a1d61955 | ||
|
|
9708e24fd2 | ||
|
|
17e910a246 | ||
|
|
0e65a2373f |
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
|
||||||
16
.github/workflows/docker-images.yml
vendored
16
.github/workflows/docker-images.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
# henrygd/beszel-agent-nvidia
|
# henrygd/beszel-agent-nvidia
|
||||||
- image: henrygd/beszel-agent-nvidia
|
- image: henrygd/beszel-agent-nvidia
|
||||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username_secret: DOCKERHUB_USERNAME
|
username_secret: DOCKERHUB_USERNAME
|
||||||
password_secret: DOCKERHUB_TOKEN
|
password_secret: DOCKERHUB_TOKEN
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
# ghcr.io/henrygd/beszel-agent-nvidia
|
# ghcr.io/henrygd/beszel-agent-nvidia
|
||||||
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
- image: ghcr.io/${{ github.repository }}/beszel-agent-nvidia
|
||||||
dockerfile: ./internal/dockerfile_agent_nvidia
|
dockerfile: ./internal/dockerfile_agent_nvidia
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64,linux/arm64
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
@@ -152,7 +152,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up bun
|
- name: Set up bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
@@ -164,14 +164,14 @@ jobs:
|
|||||||
run: bun run --cwd ./internal/site build
|
run: bun run --cwd ./internal/site build
|
||||||
|
|
||||||
- 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@v3
|
||||||
|
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ${{ matrix.image }}
|
images: ${{ matrix.image }}
|
||||||
tags: ${{ matrix.tags }}
|
tags: ${{ matrix.tags }}
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
password_secret_exists: ${{ secrets[matrix.password_secret] != '' && 'true' || 'false' }}
|
||||||
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
|
if: github.event_name != 'pull_request' && env.password_secret_exists == 'true'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
username: ${{ matrix.username || secrets[matrix.username_secret] }}
|
||||||
password: ${{ secrets[matrix.password_secret] }}
|
password: ${{ secrets[matrix.password_secret] }}
|
||||||
@@ -190,11 +190,13 @@ jobs:
|
|||||||
# Build and push Docker image with Buildx (don't push on PR)
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ${{ matrix.dockerfile }}
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
platforms: ${{ matrix.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||||
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
|
push: ${{ github.ref_type == 'tag' && secrets[matrix.password_secret] != '' }}
|
||||||
|
provenance: mode=max
|
||||||
|
sbom: true
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
|||||||
4
.github/workflows/inactivity-actions.yml
vendored
4
.github/workflows/inactivity-actions.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
name: Lock Inactive Issues
|
name: Lock Inactive Issues
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: klaasnicolaas/action-inactivity-lock@v1.1.3
|
- uses: klaasnicolaas/action-inactivity-lock@v2.0.1
|
||||||
id: lock
|
id: lock
|
||||||
with:
|
with:
|
||||||
days-inactive-issues: 14
|
days-inactive-issues: 14
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Close Stale Issues
|
- name: Close Stale Issues
|
||||||
uses: actions/stale@v10
|
uses: actions/stale@v11
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@ jobs:
|
|||||||
run: bun run --cwd ./internal/site build
|
run: bun run --cwd ./internal/site build
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: "^1.22.1"
|
go-version: "^1.22.1"
|
||||||
|
|
||||||
- name: Set up .NET
|
- name: Set up .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v6
|
||||||
with:
|
with:
|
||||||
dotnet-version: "9.0.x"
|
dotnet-version: "9.0.x"
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: GoReleaser beszel
|
- name: GoReleaser beszel
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
workdir: ./
|
workdir: ./
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
|
|||||||
4
.github/workflows/vulncheck.yml
vendored
4
.github/workflows/vulncheck.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
# cached: false
|
# cached: false
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -52,7 +52,7 @@ lint:
|
|||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -tags=testing ./...
|
go test -tags='testing no_ui' ./...
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ func newWebSocketClient(agent *Agent) (client *WebSocketClient, err error) {
|
|||||||
client = &WebSocketClient{}
|
client = &WebSocketClient{}
|
||||||
|
|
||||||
client.hubURL, err = url.Parse(hubURLStr)
|
client.hubURL, err = url.Parse(hubURLStr)
|
||||||
if err != nil {
|
if err != nil || client.hubURL.Host == "" {
|
||||||
return nil, errors.New("invalid hub URL")
|
return nil, fmt.Errorf("invalid HUB_URL %q: must include scheme and host (e.g. http://hub.example.com:8090)", hubURLStr)
|
||||||
}
|
}
|
||||||
// get registration token
|
// get registration token
|
||||||
client.token, err = getToken()
|
client.token, err = getToken()
|
||||||
|
|||||||
@@ -51,11 +51,18 @@ func TestNewWebSocketClient(t *testing.T) {
|
|||||||
errorMsg: "HUB_URL environment variable not set",
|
errorMsg: "HUB_URL environment variable not set",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid URL",
|
name: "malformed URL",
|
||||||
hubURL: "ht\ttp://invalid",
|
hubURL: "ht\ttp://invalid",
|
||||||
token: "test-token",
|
token: "test-token",
|
||||||
expectError: true,
|
expectError: true,
|
||||||
errorMsg: "invalid hub URL",
|
errorMsg: "invalid HUB_URL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL without host",
|
||||||
|
hubURL: "http:/api",
|
||||||
|
token: "test-token",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "invalid HUB_URL",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing token",
|
name: "missing token",
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ type dockerManager struct {
|
|||||||
dockerVersionChecked bool // Whether a version probe has completed successfully
|
dockerVersionChecked bool // Whether a version probe has completed successfully
|
||||||
isWindows bool // Whether the Docker Engine API is running on Windows
|
isWindows bool // Whether the Docker Engine API is running on Windows
|
||||||
buf *bytes.Buffer // Buffer to store and read response bodies
|
buf *bytes.Buffer // Buffer to store and read response bodies
|
||||||
decoder *json.Decoder // Reusable JSON decoder that reads from buf
|
|
||||||
apiStats *container.ApiStats // Reusable API stats object
|
apiStats *container.ApiStats // Reusable API stats object
|
||||||
excludeContainers []string // Patterns to exclude containers by name
|
excludeContainers []string // Patterns to exclude containers by name
|
||||||
usingPodman bool // Whether the Docker Engine API is running on Podman
|
usingPodman bool // Whether the Docker Engine API is running on Podman
|
||||||
@@ -747,20 +746,18 @@ func (dm *dockerManager) applyDockerVersionInfo(serverHeader string, versionInfo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decodes Docker API JSON response using a reusable buffer and decoder. Not thread safe.
|
// Decodes a Docker API JSON response using a reusable buffer. Not thread safe.
|
||||||
func (dm *dockerManager) decode(resp *http.Response, d any) error {
|
func (dm *dockerManager) decode(resp *http.Response, d any) error {
|
||||||
if dm.buf == nil {
|
if dm.buf == nil {
|
||||||
// initialize buffer with 256kb starting size
|
// initialize buffer with 256kb starting size
|
||||||
dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256))
|
dm.buf = bytes.NewBuffer(make([]byte, 0, 1024*256))
|
||||||
dm.decoder = json.NewDecoder(dm.buf)
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
defer dm.buf.Reset()
|
defer dm.buf.Reset()
|
||||||
_, err := dm.buf.ReadFrom(resp.Body)
|
if _, err := dm.buf.ReadFrom(resp.Body); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return dm.decoder.Decode(d)
|
return json.Unmarshal(dm.buf.Bytes(), d)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test docker / podman sockets and return if one exists
|
// Test docker / podman sockets and return if one exists
|
||||||
|
|||||||
@@ -804,6 +804,24 @@ func TestGetDockerStatsRetriesVersionCheckUntilSuccess(t *testing.T) {
|
|||||||
assert.Equal(t, 2, requestCounts["/version"])
|
assert.Equal(t, 2, requestCounts["/version"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A failed decode must not break later decodes. Previously the reused json.Decoder
|
||||||
|
// stayed desynced after one truncated response, breaking decode until restart.
|
||||||
|
func TestDecodeRecoversFromError(t *testing.T) {
|
||||||
|
dm := &dockerManager{}
|
||||||
|
|
||||||
|
// truncated JSON: body reads fine, decode fails
|
||||||
|
var bad []container.ApiInfo
|
||||||
|
err := dm.decode(&http.Response{Body: io.NopCloser(strings.NewReader(`[{"Id":"abc`))}, &bad)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// the next decode must still succeed
|
||||||
|
var good []container.ApiInfo
|
||||||
|
err = dm.decode(&http.Response{Body: io.NopCloser(strings.NewReader(`[{"Id":"abcdef012345","Names":["/ok"]}]`))}, &good)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, good, 1)
|
||||||
|
assert.Equal(t, "abcdef012345", good[0].Id)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCycleCpuDeltas(t *testing.T) {
|
func TestCycleCpuDeltas(t *testing.T) {
|
||||||
dm := &dockerManager{
|
dm := &dockerManager{
|
||||||
lastCpuContainer: map[uint16]map[string]uint64{
|
lastCpuContainer: map[uint16]map[string]uint64{
|
||||||
@@ -1003,6 +1021,44 @@ func TestCpuPercentageCalculationWithRealData(t *testing.T) {
|
|||||||
assert.InDelta(t, expectedPct, actualPct, 0.01)
|
assert.InDelta(t, expectedPct, actualPct, 0.01)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCpuPercentageHandlesCounterRollback(t *testing.T) {
|
||||||
|
// If a stats response is processed after a newer one for the same container,
|
||||||
|
// or an accounting counter resets, the current total can be lower than the
|
||||||
|
// stored previous value. Unsigned subtraction wraps to ~2^64 instead of
|
||||||
|
// going negative, so the percentage explodes, validateCpuPercentage rejects
|
||||||
|
// the sample, and the whole collection is discarded - network stats too.
|
||||||
|
stats := &container.ApiStats{
|
||||||
|
CPUStats: container.CPUStats{
|
||||||
|
CPUUsage: container.CPUUsage{TotalUsage: 1_000_000},
|
||||||
|
SystemUsage: 20_000_000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container counter went backwards.
|
||||||
|
assert.Equal(t, 0.0, stats.CalculateCpuPercentLinux(2_000_000, 10_000_000))
|
||||||
|
// System counter went backwards.
|
||||||
|
assert.Equal(t, 0.0, stats.CalculateCpuPercentLinux(500_000, 30_000_000))
|
||||||
|
// A normal forward sample is unaffected: 500000 / 10000000 * 100 = 5%.
|
||||||
|
assert.InDelta(t, 5.0, stats.CalculateCpuPercentLinux(500_000, 10_000_000), 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCpuPercentageWindowsHandlesCounterRollback(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
stats := &container.ApiStats{
|
||||||
|
Read: now,
|
||||||
|
NumProcs: 4,
|
||||||
|
CPUStats: container.CPUStats{
|
||||||
|
CPUUsage: container.CPUUsage{TotalUsage: 1_000_000},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
prevRead := now.Add(-time.Second)
|
||||||
|
|
||||||
|
// Container counter went backwards.
|
||||||
|
assert.Equal(t, 0.0, stats.CalculateCpuPercentWindows(2_000_000, prevRead))
|
||||||
|
// A normal forward sample is unaffected.
|
||||||
|
assert.Greater(t, stats.CalculateCpuPercentWindows(500_000, prevRead), 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNetworkStatsCalculationWithRealData(t *testing.T) {
|
func TestNetworkStatsCalculationWithRealData(t *testing.T) {
|
||||||
// Create synthetic test data to avoid timing issues
|
// Create synthetic test data to avoid timing issues
|
||||||
apiStats1 := &container.ApiStats{
|
apiStats1 := &container.ApiStats{
|
||||||
|
|||||||
@@ -180,8 +180,13 @@ func mdraidSmartStatus(health mdraidHealth) string {
|
|||||||
if health.degraded > 0 {
|
if health.degraded > 0 {
|
||||||
return "FAILED"
|
return "FAILED"
|
||||||
}
|
}
|
||||||
|
if health.mismatchCnt > 0 {
|
||||||
|
return "WARNING"
|
||||||
|
}
|
||||||
|
// "check" scans for consistency problems without repairing mismatches.
|
||||||
|
// With no mismatches, keep it green while reporting progress attributes.
|
||||||
switch syncAction {
|
switch syncAction {
|
||||||
case "check", "repair":
|
case "repair":
|
||||||
return "WARNING"
|
return "WARNING"
|
||||||
}
|
}
|
||||||
switch state {
|
switch state {
|
||||||
|
|||||||
@@ -94,6 +94,18 @@ func TestMdraidSmartStatus(t *testing.T) {
|
|||||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", syncAction: "recover"}); got != "WARNING" {
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "active", syncAction: "recover"}); got != "WARNING" {
|
||||||
t.Fatalf("mdraidSmartStatus(recover) = %q, want WARNING", got)
|
t.Fatalf("mdraidSmartStatus(recover) = %q, want WARNING", got)
|
||||||
}
|
}
|
||||||
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", syncAction: "check"}); got != "PASSED" {
|
||||||
|
t.Fatalf("mdraidSmartStatus(clean+check) = %q, want PASSED", got)
|
||||||
|
}
|
||||||
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", syncAction: "check", mismatchCnt: 1}); got != "WARNING" {
|
||||||
|
t.Fatalf("mdraidSmartStatus(clean+check+mismatch) = %q, want WARNING", got)
|
||||||
|
}
|
||||||
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", mismatchCnt: 1}); got != "WARNING" {
|
||||||
|
t.Fatalf("mdraidSmartStatus(clean+mismatch) = %q, want WARNING", got)
|
||||||
|
}
|
||||||
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean", syncAction: "repair"}); got != "WARNING" {
|
||||||
|
t.Fatalf("mdraidSmartStatus(repair) = %q, want WARNING", got)
|
||||||
|
}
|
||||||
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean"}); got != "PASSED" {
|
if got := mdraidSmartStatus(mdraidHealth{arrayState: "clean"}); got != "PASSED" {
|
||||||
t.Fatalf("mdraidSmartStatus(clean) = %q, want PASSED", got)
|
t.Fatalf("mdraidSmartStatus(clean) = %q, want PASSED", got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ type ServerOptions struct {
|
|||||||
Keys []gossh.PublicKey // SSH public keys for authentication
|
Keys []gossh.PublicKey // SSH public keys for authentication
|
||||||
}
|
}
|
||||||
|
|
||||||
// hubVersions caches hub versions by session ID to avoid repeated parsing.
|
|
||||||
var hubVersions map[string]semver.Version
|
|
||||||
|
|
||||||
// StartServer starts the SSH server with the provided options.
|
// StartServer starts the SSH server with the provided options.
|
||||||
// It configures the server with secure defaults, sets up authentication,
|
// It configures the server with secure defaults, sets up authentication,
|
||||||
// and begins listening for connections. Returns an error if the server
|
// and begins listening for connections. Returns an error if the server
|
||||||
@@ -99,24 +96,15 @@ func (a *Agent) StartServer(opts ServerOptions) error {
|
|||||||
return a.server.Serve(ln)
|
return a.server.Serve(ln)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHubVersion retrieves and caches the hub version for a given session.
|
// getHubVersion extracts the hub version from the SSH client version string
|
||||||
// It extracts the version from the SSH client version string and caches
|
// for a given session. Returns a zero version if parsing fails.
|
||||||
// it to avoid repeated parsing. Returns a zero version if parsing fails.
|
func (a *Agent) getHubVersion(sessionCtx ssh.Context) semver.Version {
|
||||||
func (a *Agent) getHubVersion(sessionId string, sessionCtx ssh.Context) semver.Version {
|
|
||||||
if hubVersions == nil {
|
|
||||||
hubVersions = make(map[string]semver.Version, 1)
|
|
||||||
}
|
|
||||||
hubVersion, ok := hubVersions[sessionId]
|
|
||||||
if ok {
|
|
||||||
return hubVersion
|
|
||||||
}
|
|
||||||
// Extract hub version from SSH client version
|
|
||||||
clientVersion := sessionCtx.Value(ssh.ContextKeyClientVersion)
|
clientVersion := sessionCtx.Value(ssh.ContextKeyClientVersion)
|
||||||
if versionStr, ok := clientVersion.(string); ok {
|
if versionStr, ok := clientVersion.(string); ok {
|
||||||
hubVersion, _ = extractHubVersion(versionStr)
|
hubVersion, _ := extractHubVersion(versionStr)
|
||||||
|
return hubVersion
|
||||||
}
|
}
|
||||||
hubVersions[sessionId] = hubVersion
|
return semver.Version{}
|
||||||
return hubVersion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSession handles an incoming SSH session by gathering system statistics
|
// handleSession handles an incoming SSH session by gathering system statistics
|
||||||
@@ -127,9 +115,8 @@ func (a *Agent) handleSession(s ssh.Session) {
|
|||||||
a.connectionManager.eventChan <- SSHConnect
|
a.connectionManager.eventChan <- SSHConnect
|
||||||
|
|
||||||
sessionCtx := s.Context()
|
sessionCtx := s.Context()
|
||||||
sessionID := sessionCtx.SessionID()
|
|
||||||
|
|
||||||
hubVersion := a.getHubVersion(sessionID, sessionCtx)
|
hubVersion := a.getHubVersion(sessionCtx)
|
||||||
|
|
||||||
// Legacy one-shot behavior for older hubs
|
// Legacy one-shot behavior for older hubs
|
||||||
if hubVersion.LT(beszel.MinVersionAgentResponse) {
|
if hubVersion.LT(beszel.MinVersionAgentResponse) {
|
||||||
|
|||||||
@@ -404,27 +404,23 @@ func TestGetHubVersion(t *testing.T) {
|
|||||||
clientVersion: "SSH-2.0-beszel_0.12.0",
|
clientVersion: "SSH-2.0-beszel_0.12.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test first call - should extract and cache version
|
// Test first call - should extract version
|
||||||
version := agent.getHubVersion("test-session-123", mockCtx)
|
version := agent.getHubVersion(mockCtx)
|
||||||
assert.Equal(t, "0.12.0", version.String())
|
assert.Equal(t, "0.12.0", version.String())
|
||||||
|
|
||||||
// Test second call - should return cached version
|
// Test that version reflects the current client version (no stale caching)
|
||||||
mockCtx.clientVersion = "SSH-2.0-beszel_0.11.0" // Change version but should still return cached
|
mockCtx.clientVersion = "SSH-2.0-beszel_0.11.0"
|
||||||
version = agent.getHubVersion("test-session-123", mockCtx)
|
version = agent.getHubVersion(mockCtx)
|
||||||
assert.Equal(t, "0.12.0", version.String()) // Should still be cached version
|
|
||||||
|
|
||||||
// Test different session - should extract new version
|
|
||||||
version = agent.getHubVersion("different-session", mockCtx)
|
|
||||||
assert.Equal(t, "0.11.0", version.String())
|
assert.Equal(t, "0.11.0", version.String())
|
||||||
|
|
||||||
// Test with invalid version string (non-beszel client)
|
// Test with invalid version string (non-beszel client)
|
||||||
mockCtx.clientVersion = "SSH-2.0-OpenSSH_8.0"
|
mockCtx.clientVersion = "SSH-2.0-OpenSSH_8.0"
|
||||||
version = agent.getHubVersion("invalid-session", mockCtx)
|
version = agent.getHubVersion(mockCtx)
|
||||||
assert.Equal(t, "0.0.0", version.String()) // Should be empty version for non-beszel clients
|
assert.Equal(t, "0.0.0", version.String()) // Should be empty version for non-beszel clients
|
||||||
|
|
||||||
// Test with no client version
|
// Test with no client version
|
||||||
mockCtx.clientVersion = ""
|
mockCtx.clientVersion = ""
|
||||||
version = agent.getHubVersion("no-version-session", mockCtx)
|
version = agent.getHubVersion(mockCtx)
|
||||||
assert.True(t, version.EQ(semver.Version{})) // Should be empty version
|
assert.True(t, version.EQ(semver.Version{})) // Should be empty version
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,9 +497,6 @@ func TestWriteToSessionEncoding(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Reset the global hubVersions map to ensure clean state for each test
|
|
||||||
hubVersions = nil
|
|
||||||
|
|
||||||
agent, err := NewAgent("")
|
agent, err := NewAgent("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -585,39 +578,28 @@ func createTestCombinedData() *system.CombinedData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHubVersionCaching(t *testing.T) {
|
// TestGetHubVersionConcurrent guards against a regression of the
|
||||||
// Reset the global hubVersions map to ensure clean state
|
// "concurrent map writes" panic previously caused by a shared, unsynchronized
|
||||||
hubVersions = nil
|
// hubVersions cache (see https://github.com/henrygd/beszel/issues/2128).
|
||||||
|
// getHubVersion no longer shares mutable state between sessions, so calling
|
||||||
|
// it concurrently from many goroutines must be safe under `go test -race`.
|
||||||
|
func TestGetHubVersionConcurrent(t *testing.T) {
|
||||||
agent, err := NewAgent("")
|
agent, err := NewAgent("")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx1 := &mockSSHContext{
|
const goroutines = 50
|
||||||
sessionID: "session1",
|
var wg sync.WaitGroup
|
||||||
clientVersion: "SSH-2.0-beszel_0.12.0",
|
wg.Add(goroutines)
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
ctx := &mockSSHContext{
|
||||||
|
sessionID: fmt.Sprintf("session-%d", i),
|
||||||
|
clientVersion: "SSH-2.0-beszel_0.12.0",
|
||||||
|
}
|
||||||
|
version := agent.getHubVersion(ctx)
|
||||||
|
assert.Equal(t, "0.12.0", version.String())
|
||||||
|
}(i)
|
||||||
}
|
}
|
||||||
ctx2 := &mockSSHContext{
|
wg.Wait()
|
||||||
sessionID: "session2",
|
|
||||||
clientVersion: "SSH-2.0-beszel_0.11.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
// First calls should cache the versions
|
|
||||||
v1 := agent.getHubVersion("session1", ctx1)
|
|
||||||
v2 := agent.getHubVersion("session2", ctx2)
|
|
||||||
|
|
||||||
assert.Equal(t, "0.12.0", v1.String())
|
|
||||||
assert.Equal(t, "0.11.0", v2.String())
|
|
||||||
|
|
||||||
// Verify caching by changing context but keeping same session ID
|
|
||||||
ctx1.clientVersion = "SSH-2.0-beszel_0.10.0"
|
|
||||||
v1Cached := agent.getHubVersion("session1", ctx1)
|
|
||||||
assert.Equal(t, "0.12.0", v1Cached.String()) // Should still be cached version
|
|
||||||
|
|
||||||
// New session should get new version
|
|
||||||
ctx3 := &mockSSHContext{
|
|
||||||
sessionID: "session3",
|
|
||||||
clientVersion: "SSH-2.0-beszel_0.13.0",
|
|
||||||
}
|
|
||||||
v3 := agent.getHubVersion("session3", ctx3)
|
|
||||||
assert.Equal(t, "0.13.0", v3.String())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,21 +169,11 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
|
|
||||||
// memory
|
// memory
|
||||||
if v, err := mem.VirtualMemory(); err == nil {
|
if v, err := mem.VirtualMemory(); err == nil {
|
||||||
|
used, cacheBuff, swapUsed := calculateHostMemoryUsage(v, a.memCalc == "htop")
|
||||||
// swap
|
// swap
|
||||||
systemStats.Swap = utils.BytesToGigabytes(v.SwapTotal)
|
systemStats.Swap = utils.BytesToGigabytes(v.SwapTotal)
|
||||||
systemStats.SwapUsed = utils.BytesToGigabytes(v.SwapTotal - v.SwapFree - v.SwapCached)
|
systemStats.SwapUsed = utils.BytesToGigabytes(swapUsed)
|
||||||
// cache + buffers value for default mem calculation
|
v.Used = used
|
||||||
// note: gopsutil automatically adds SReclaimable to v.Cached
|
|
||||||
cacheBuff := v.Cached + v.Buffers - v.Shared
|
|
||||||
if cacheBuff <= 0 {
|
|
||||||
cacheBuff = max(v.Total-v.Free-v.Used, 0)
|
|
||||||
}
|
|
||||||
// htop memory calculation overrides (likely outdated as of mid 2025)
|
|
||||||
if a.memCalc == "htop" {
|
|
||||||
// cacheBuff = v.Cached + v.Buffers - v.Shared
|
|
||||||
v.Used = v.Total - (v.Free + cacheBuff)
|
|
||||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
|
||||||
}
|
|
||||||
// if a.memCalc == "legacy" {
|
// if a.memCalc == "legacy" {
|
||||||
// v.Used = v.Total - v.Free - v.Buffers - v.Cached
|
// v.Used = v.Total - v.Free - v.Buffers - v.Cached
|
||||||
// cacheBuff = v.Total - v.Free - v.Used
|
// cacheBuff = v.Total - v.Free - v.Used
|
||||||
@@ -193,10 +183,14 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
if a.zfs {
|
if a.zfs {
|
||||||
if arcSize, _ := zfs.ARCSize(); arcSize > 0 && arcSize < v.Used {
|
if arcSize, _ := zfs.ARCSize(); arcSize > 0 && arcSize < v.Used {
|
||||||
v.Used = v.Used - arcSize
|
v.Used = v.Used - arcSize
|
||||||
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
|
||||||
systemStats.MemZfsArc = utils.BytesToGigabytes(arcSize)
|
systemStats.MemZfsArc = utils.BytesToGigabytes(arcSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v.Total > 0 {
|
||||||
|
v.UsedPercent = float64(v.Used) / float64(v.Total) * 100.0
|
||||||
|
} else {
|
||||||
|
v.UsedPercent = 0
|
||||||
|
}
|
||||||
systemStats.Mem = utils.BytesToGigabytes(v.Total)
|
systemStats.Mem = utils.BytesToGigabytes(v.Total)
|
||||||
systemStats.MemBuffCache = utils.BytesToGigabytes(cacheBuff)
|
systemStats.MemBuffCache = utils.BytesToGigabytes(cacheBuff)
|
||||||
systemStats.MemUsed = utils.BytesToGigabytes(v.Used)
|
systemStats.MemUsed = utils.BytesToGigabytes(v.Used)
|
||||||
@@ -263,6 +257,38 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
return systemStats
|
return systemStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculateHostMemoryUsage derives counters defensively because /proc/meminfo may
|
||||||
|
// change while gopsutil reads it. Invalid unsigned subtractions saturate at zero.
|
||||||
|
func calculateHostMemoryUsage(v *mem.VirtualMemoryStat, htop bool) (used, cacheBuff, swapUsed uint64) {
|
||||||
|
used = v.Used
|
||||||
|
if used > v.Total {
|
||||||
|
used = saturatingSub(v.Total, v.Available)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gopsutil automatically adds SReclaimable to Cached.
|
||||||
|
cacheBuff = min(v.Cached, v.Total)
|
||||||
|
cacheBuff += min(v.Buffers, v.Total-cacheBuff)
|
||||||
|
cacheBuff = saturatingSub(cacheBuff, min(v.Shared, v.Total))
|
||||||
|
if v.Cached == 0 && v.Buffers == 0 {
|
||||||
|
cacheBuff = saturatingSub(v.Total, v.Free, used)
|
||||||
|
}
|
||||||
|
if htop {
|
||||||
|
used = saturatingSub(v.Total, v.Free, cacheBuff)
|
||||||
|
}
|
||||||
|
return used, cacheBuff, saturatingSub(v.SwapTotal, v.SwapFree, v.SwapCached)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saturatingSub subtracts each value, returning zero on underflow.
|
||||||
|
func saturatingSub(value uint64, subtrahends ...uint64) uint64 {
|
||||||
|
for _, subtrahend := range subtrahends {
|
||||||
|
if subtrahend > value {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
value -= subtrahend
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
|
// getOsPrettyName attempts to get the pretty OS name from /etc/os-release on Linux systems
|
||||||
func getOsPrettyName() (string, error) {
|
func getOsPrettyName() (string, error) {
|
||||||
file, err := os.Open("/etc/os-release")
|
file, err := os.Open("/etc/os-release")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
"github.com/shirou/gopsutil/v4/mem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -33,6 +34,59 @@ func TestGatherStatsDoesNotAttachDetailsToCachedRequests(t *testing.T) {
|
|||||||
assert.Nil(t, secondResponse.Details)
|
assert.Nil(t, secondResponse.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalculateHostMemoryUsage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
memory mem.VirtualMemoryStat
|
||||||
|
htop bool
|
||||||
|
used, cacheBuff, swapUsed uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
memory: mem.VirtualMemoryStat{Total: 100, Available: 40, Used: 60, Free: 20, Cached: 25, Buffers: 10, Shared: 5, SwapTotal: 20, SwapFree: 8, SwapCached: 2},
|
||||||
|
used: 60,
|
||||||
|
cacheBuff: 30,
|
||||||
|
swapUsed: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inconsistent counters saturate",
|
||||||
|
memory: mem.VirtualMemoryStat{Total: 100, Available: 110, Used: ^uint64(0) - 9, Free: 90, Cached: 5, Buffers: 10, Shared: 20, SwapTotal: 10, SwapFree: 9, SwapCached: 2},
|
||||||
|
used: 0,
|
||||||
|
cacheBuff: 0,
|
||||||
|
swapUsed: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "htop subtraction saturates",
|
||||||
|
memory: mem.VirtualMemoryStat{Total: 100, Available: 20, Used: 80, Free: 90, Cached: 20, Buffers: 5, SwapTotal: 30, SwapFree: 10, SwapCached: 5},
|
||||||
|
htop: true,
|
||||||
|
used: 0,
|
||||||
|
cacheBuff: 25,
|
||||||
|
swapUsed: 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero cache from shared cancellation does not fall back",
|
||||||
|
memory: mem.VirtualMemoryStat{Total: 100, Used: 60, Free: 10, Cached: 20, Buffers: 10, Shared: 30},
|
||||||
|
used: 60,
|
||||||
|
cacheBuff: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absent cache counters use fallback",
|
||||||
|
memory: mem.VirtualMemoryStat{Total: 100, Used: 60, Free: 10},
|
||||||
|
used: 60,
|
||||||
|
cacheBuff: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
used, cacheBuff, swapUsed := calculateHostMemoryUsage(&tt.memory, tt.htop)
|
||||||
|
assert.Equal(t, tt.used, used)
|
||||||
|
assert.Equal(t, tt.cacheBuff, cacheBuff)
|
||||||
|
assert.Equal(t, tt.swapUsed, swapUsed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
|
func TestUpdateSystemDetailsMarksDetailsDirty(t *testing.T) {
|
||||||
agent := &Agent{}
|
agent := &Agent{}
|
||||||
|
|
||||||
|
|||||||
57
go.mod
57
go.mod
@@ -1,26 +1,27 @@
|
|||||||
module github.com/henrygd/beszel
|
module github.com/henrygd/beszel
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/coreos/go-systemd/v22 v22.7.0
|
github.com/coreos/go-systemd/v22 v22.7.0
|
||||||
github.com/ebitengine/purego v0.10.0
|
github.com/ebitengine/purego v0.10.2
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0
|
github.com/fxamacker/cbor/v2 v2.9.2
|
||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lxzan/gws v1.9.1
|
github.com/lxzan/gws v1.10.1
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3
|
github.com/nicholas-fedor/shoutrrr v0.17.0
|
||||||
github.com/pocketbase/dbx v1.12.0
|
github.com/pocketbase/dbx v1.12.0
|
||||||
github.com/pocketbase/pocketbase v0.36.8
|
github.com/pocketbase/pocketbase v0.39.10
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3
|
github.com/shirou/gopsutil/v4 v4.26.7
|
||||||
github.com/spf13/cast v1.10.0
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.55.0
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
golang.org/x/exp v0.0.0-20260811152304-ee035b5b010f
|
||||||
golang.org/x/sys v0.42.0
|
golang.org/x/net v0.57.0
|
||||||
|
golang.org/x/sys v0.47.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
)
|
)
|
||||||
@@ -34,35 +35,35 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/eclipse/paho.golang v0.23.0 // indirect
|
github.com/eclipse/paho.golang v0.23.0 // indirect
|
||||||
github.com/fatih/color v1.19.0 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||||
|
github.com/ganigeorgiev/fexpr v0.6.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
|
||||||
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
github.com/go-sql-driver/mysql v1.9.1 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.5 // indirect
|
github.com/klauspost/compress v1.19.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.15 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/pocketbase/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
golang.org/x/image v0.38.0 // indirect
|
golang.org/x/image v0.45.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.org/x/term v0.41.0 // indirect
|
golang.org/x/term v0.45.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.41.0 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.74.1 // 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.12.0 // indirect
|
||||||
modernc.org/sqlite v1.48.0 // indirect
|
modernc.org/sqlite v1.55.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
162
go.sum
162
go.sum
@@ -1,7 +1,7 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||||
@@ -21,29 +21,29 @@ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCO
|
|||||||
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvSyR6Mk=
|
github.com/eclipse/paho.golang v0.23.0 h1:KHgl2wz6EJo7cMBmkuhpt7C576vP+kpPv7jjvSyR6Mk=
|
||||||
github.com/eclipse/paho.golang v0.23.0/go.mod h1:nQRhTkoZv8EAiNs5UU0/WdQIx2NrnWUpL9nsGJTQN04=
|
github.com/eclipse/paho.golang v0.23.0/go.mod h1:nQRhTkoZv8EAiNs5UU0/WdQIx2NrnWUpL9nsGJTQN04=
|
||||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||||
|
github.com/ganigeorgiev/fexpr v0.6.0 h1:Fza3O/QMBKEudUvxV862qe6GjxM60GJjjKytdp+VQus=
|
||||||
|
github.com/ganigeorgiev/fexpr v0.6.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
@@ -56,8 +56,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
|
|||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
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/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
@@ -66,47 +66,49 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
|||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A=
|
github.com/jarcoal/httpmock v1.4.2 h1:dKwiP/9zITCPfBLsDn3kchbSOu16JrnxtVEmL0fPRcI=
|
||||||
github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
github.com/jarcoal/httpmock v1.4.2/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.19.2/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/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 h1:Qj3hTcdWH8uMZDI41HNuTuJN525C7NBrbtH5kSO6fPk=
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0=
|
||||||
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
github.com/lxzan/gws v1.9.1 h1:4lbIp4cW0hOLP3ejFHR/uWRy741AURx7oKkNNi2OT9o=
|
github.com/lxzan/gws v1.10.1 h1:1xG+tDOV0lgDeVPf0wNT74u3cn0K3LpcavRrTPTrMwQ=
|
||||||
github.com/lxzan/gws v1.9.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
github.com/lxzan/gws v1.10.1/go.mod h1:gXHSCPmTGryWJ4icuqy8Yho32E4YIMHH0fkDRYJRbdc=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3 h1:aBX2iw9a7jl5wfHd3bi9LnS5ucoYIy6KcLH9XVF+gig=
|
github.com/nicholas-fedor/shoutrrr v0.17.0 h1:xfp3z5QbE8jXvUhUEwWDk47SJ/b912VoB8MJJDU+q4E=
|
||||||
github.com/nicholas-fedor/shoutrrr v0.14.3/go.mod h1:U7IywBkLpBV7rgn8iLbQ9/LklJG1gm24bFv5cXXsDKs=
|
github.com/nicholas-fedor/shoutrrr v0.17.0/go.mod h1:s4ldyLs6uwBy9lIjYrY+8lyTqJtPvZSrILw0CyMLock=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
|
||||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
|
||||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
|
||||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
|
||||||
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
|
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
|
||||||
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
||||||
github.com/pocketbase/pocketbase v0.36.8 h1:gCNqoesZ44saYOD3J7edhi5nDwUWKyQG7boM/kVwz2c=
|
github.com/pocketbase/ozzo-validation/v4 v4.3.0 h1:uKBDVma7bZqgR2a6AwE+k9hkuDFfiZMpBHQdZ1z3iQs=
|
||||||
github.com/pocketbase/pocketbase v0.36.8/go.mod h1:OY4WaXbP0WnF/EXoBbboWJK+ZSZ1A85tiA0sjrTKxTA=
|
github.com/pocketbase/ozzo-validation/v4 v4.3.0/go.mod h1:6XNjSTw/Jb2F8LOkKO3oyzIWExbrGiYoS4uVxVwz90g=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/pocketbase/pocketbase v0.39.10 h1:2j8TDJRuo3aAC8Y8F9WFux0SwYcxeDCgEYQxxdWkwGE=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/pocketbase/pocketbase v0.39.10/go.mod h1:tSX3anHQ7Ul6dPV9WhlEc6No1DtklGF69iwnVNW3BEE=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
@@ -120,51 +122,51 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
golang.org/x/exp v0.0.0-20260811152304-ee035b5b010f h1:iXpLj9sdDH/RLYsnOMpbETK6KWtrHwvegcc4psWJHV8=
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
golang.org/x/exp v0.0.0-20260811152304-ee035b5b010f/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
|
||||||
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
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=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -175,30 +177,30 @@ 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=
|
||||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
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.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.4/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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||||
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.12.0 h1:twkmYNkGXCvtYWzoux02jtK6eovjZbdI0uHFUYp6kuU=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.12.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.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
|
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||||
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||||
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=
|
||||||
|
|||||||
@@ -322,8 +322,9 @@ func TestAlertSilencedMultiUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertSilencedWithActualAlert(t *testing.T) {
|
func TestAlertSilencedWithActualAlert(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
// Create a system
|
// Create a system
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ func setStatusAlertEmail(t *testing.T, hub core.App, userID, email string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAlerts(t *testing.T) {
|
func TestStatusAlerts(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
|
systems, err := beszelTests.CreateSystems(hub, 4, user.Id, "paused")
|
||||||
@@ -234,8 +235,9 @@ func TestHandleStatusAlertsDoesNotSendRecoveryWhileDownIsOnlyPending(t *testing.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAlertTimerCancellationPreventsBoundaryDelivery(t *testing.T) {
|
func TestStatusAlertTimerCancellationPreventsBoundaryDelivery(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
userSettings, err := hub.FindFirstRecordByFilter("user_settings", "user={:user}", map[string]any{"user": user.Id})
|
userSettings, err := hub.FindFirstRecordByFilter("user_settings", "user={:user}", map[string]any{"user": user.Id})
|
||||||
@@ -336,8 +338,9 @@ func TestStatusAlertDownFiresAfterDelayExpires(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAlertMultipleUsersRespectDifferentMinutes(t *testing.T) {
|
func TestStatusAlertMultipleUsersRespectDifferentMinutes(t *testing.T) {
|
||||||
|
hub, user1 := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user1 := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
setStatusAlertEmail(t, hub, user1.Id, "user1@example.com")
|
setStatusAlertEmail(t, hub, user1.Id, "user1@example.com")
|
||||||
@@ -423,8 +426,9 @@ func TestStatusAlertMultipleUsersRespectDifferentMinutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAlertMultipleUsersRecoveryBetweenMinutesOnlyAlertsEarlierUser(t *testing.T) {
|
func TestStatusAlertMultipleUsersRecoveryBetweenMinutesOnlyAlertsEarlierUser(t *testing.T) {
|
||||||
|
hub, user1 := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user1 := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
setStatusAlertEmail(t, hub, user1.Id, "user1@example.com")
|
setStatusAlertEmail(t, hub, user1.Id, "user1@example.com")
|
||||||
@@ -816,8 +820,9 @@ func TestResolveStatusAlerts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertsHistoryStatus(t *testing.T) {
|
func TestAlertsHistoryStatus(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
// Create a system
|
// Create a system
|
||||||
@@ -882,8 +887,9 @@ func TestAlertsHistoryStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusAlertClearedBeforeSend(t *testing.T) {
|
func TestStatusAlertClearedBeforeSend(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
// Create a system
|
// Create a system
|
||||||
|
|||||||
@@ -95,11 +95,10 @@ func waitForSystemAlert(d time.Duration) {
|
|||||||
|
|
||||||
func testOneMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, setValue systemAlertValueSetter[T], triggerValue, resolveValue T) {
|
func testOneMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, setValue systemAlertValueSetter[T], triggerValue, resolveValue T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
fixture := newSystemAlertTestFixture(t, alertName, 1, threshold)
|
||||||
|
defer fixture.cleanup()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
fixture := newSystemAlertTestFixture(t, alertName, 1, threshold)
|
|
||||||
defer fixture.cleanup()
|
|
||||||
|
|
||||||
submitValue(fixture, t, triggerValue, setValue)
|
submitValue(fixture, t, triggerValue, setValue)
|
||||||
waitForSystemAlert(time.Second)
|
waitForSystemAlert(time.Second)
|
||||||
|
|
||||||
@@ -118,11 +117,10 @@ func testOneMinuteSystemAlert[T any](t *testing.T, alertName string, threshold f
|
|||||||
|
|
||||||
func testMultiMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, min int, setValue systemAlertValueSetter[T], baselineValue, triggerValue, resolveValue T) {
|
func testMultiMinuteSystemAlert[T any](t *testing.T, alertName string, threshold float64, min int, setValue systemAlertValueSetter[T], baselineValue, triggerValue, resolveValue T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
fixture := newSystemAlertTestFixture(t, alertName, min, threshold)
|
||||||
|
defer fixture.cleanup()
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
fixture := newSystemAlertTestFixture(t, alertName, min, threshold)
|
|
||||||
defer fixture.cleanup()
|
|
||||||
|
|
||||||
submitValue(fixture, t, baselineValue, setValue)
|
submitValue(fixture, t, baselineValue, setValue)
|
||||||
waitForSystemAlert(time.Minute + time.Second)
|
waitForSystemAlert(time.Minute + time.Second)
|
||||||
fixture.assertTriggered(t, false, "Alert should not be triggered yet")
|
fixture.assertTriggered(t, false, "Alert should not be triggered yet")
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertsHistory(t *testing.T) {
|
func TestAlertsHistory(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
|
||||||
synctest.Test(t, func(t *testing.T) {
|
synctest.Test(t, func(t *testing.T) {
|
||||||
hub, user := beszelTests.GetHubWithUser(t)
|
|
||||||
defer hub.Cleanup()
|
defer hub.Cleanup()
|
||||||
|
|
||||||
// Create systems and alerts
|
// Create systems and alerts
|
||||||
|
|||||||
@@ -52,6 +52,17 @@ type HostInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
||||||
|
// A counter can read lower than the stored previous value when a stats
|
||||||
|
// response is processed after a newer one for the same container, or when an
|
||||||
|
// accounting counter resets. Unsigned subtraction wraps to ~2^64 instead of
|
||||||
|
// going negative: on the container counter that surfaces as an absurd
|
||||||
|
// percentage the caller rejects, discarding the whole sample; on the system
|
||||||
|
// counter it inflates the divisor and silently reports near-zero CPU.
|
||||||
|
// Treat either direction as a new baseline.
|
||||||
|
if s.CPUStats.CPUUsage.TotalUsage < prevCpuContainer || s.CPUStats.SystemUsage < prevCpuSystem {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
||||||
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
||||||
|
|
||||||
@@ -70,7 +81,11 @@ func (s *ApiStats) CalculateCpuPercentWindows(prevCpuUsage uint64, prevRead time
|
|||||||
possIntervals /= 100 // Convert to number of 100ns intervals
|
possIntervals /= 100 // Convert to number of 100ns intervals
|
||||||
possIntervals *= uint64(s.NumProcs) // Multiple by the number of processors
|
possIntervals *= uint64(s.NumProcs) // Multiple by the number of processors
|
||||||
|
|
||||||
// Intervals used
|
// Intervals used. Same rollback guard as the Linux path: an out-of-order or
|
||||||
|
// reset counter would wrap the subtraction to ~2^64.
|
||||||
|
if s.CPUStats.CPUUsage.TotalUsage < prevCpuUsage {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
intervalsUsed := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage
|
intervalsUsed := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage
|
||||||
|
|
||||||
// Percentage avoiding divide-by-zero
|
// Percentage avoiding divide-by-zero
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func createConfigTestFingerprint(app core.App, systemID, token, fingerprint stri
|
|||||||
|
|
||||||
// TestConfigSyncWithTokens tests the config.SyncSystems function with various token scenarios
|
// TestConfigSyncWithTokens tests the config.SyncSystems function with various token scenarios
|
||||||
func TestConfigSyncWithTokens(t *testing.T) {
|
func TestConfigSyncWithTokens(t *testing.T) {
|
||||||
testHub, err := tests.NewTestHub()
|
testHub, err := tests.NewTestHub(t.TempDir())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer testHub.Cleanup()
|
defer testHub.Cleanup()
|
||||||
|
|
||||||
|
|||||||
56
internal/hub/systems/ssh_timeout_test.go
Normal file
56
internal/hub/systems/ssh_timeout_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//go:build testing
|
||||||
|
|
||||||
|
package systems
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRunWithTimeout covers the guard added for issue #2041: the per-system SSH
|
||||||
|
// data exchange must never block the updater indefinitely on a dead connection.
|
||||||
|
func TestRunWithTimeout(t *testing.T) {
|
||||||
|
t.Run("returns the operation result when it completes before the timeout", func(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
wantErr := errors.New("boom")
|
||||||
|
onTimeoutCalled := false
|
||||||
|
|
||||||
|
retry, err := runWithTimeout(10*time.Second, func() (bool, error) {
|
||||||
|
return true, wantErr
|
||||||
|
}, func() { onTimeoutCalled = true })
|
||||||
|
|
||||||
|
assert.True(t, retry, "should return the operation's retry value")
|
||||||
|
assert.Equal(t, wantErr, err, "should return the operation's error")
|
||||||
|
assert.False(t, onTimeoutCalled, "onTimeout must not fire when the op completes")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("times out and tears down the connection when the op blocks", func(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
// unblock simulates a half-open connection: the op is stuck reading a
|
||||||
|
// response that never arrives until the connection is torn down.
|
||||||
|
unblock := make(chan struct{})
|
||||||
|
onTimeoutCalled := false
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
retry, err := runWithTimeout(5*time.Second, func() (bool, error) {
|
||||||
|
<-unblock
|
||||||
|
return false, nil
|
||||||
|
}, func() {
|
||||||
|
onTimeoutCalled = true
|
||||||
|
close(unblock) // tearing down the connection releases the blocked read
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, 5*time.Second, time.Since(start), "should return exactly at the timeout")
|
||||||
|
assert.True(t, retry, "a timeout should be retryable so the next tick re-dials")
|
||||||
|
assert.Error(t, err, "a timeout must surface an error so the system is set down")
|
||||||
|
assert.True(t, onTimeoutCalled, "onTimeout must fire so the dead connection is closed")
|
||||||
|
|
||||||
|
synctest.Wait() // ensure the released op goroutine exits cleanly
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -388,6 +388,7 @@ func (sys *System) setDown(originalError error) error {
|
|||||||
if originalError != nil {
|
if originalError != nil {
|
||||||
sys.manager.hub.Logger().Error("System down", "system", record.GetString("name"), "err", originalError)
|
sys.manager.hub.Logger().Error("System down", "system", record.GetString("name"), "err", originalError)
|
||||||
}
|
}
|
||||||
|
sys.detailsFetched.Store(false)
|
||||||
record.Set("status", down)
|
record.Set("status", down)
|
||||||
return sys.manager.hub.SaveNoValidate(record)
|
return sys.manager.hub.SaveNoValidate(record)
|
||||||
}
|
}
|
||||||
@@ -623,10 +624,17 @@ func (sys *System) runSSHOperation(timeout time.Duration, retries int, operation
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
retry, opErr := func() (bool, error) {
|
// Bound the whole operation. A half-open TCP connection (a dead peer that
|
||||||
|
// never sends RST/FIN) or a wedged agent that accepts the session but
|
||||||
|
// never writes a response would otherwise block the read forever. Because
|
||||||
|
// StartUpdater runs update() synchronously on its ticker, that stalls the
|
||||||
|
// per-system updater indefinitely with no error and no re-dial until the
|
||||||
|
// hub is restarted (issue #2041). On timeout we tear down the connection
|
||||||
|
// so the blocked read unwinds and the system is re-dialed on the next tick.
|
||||||
|
retry, opErr := runWithTimeout(sshOperationTimeout, func() (bool, error) {
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
return operation(session)
|
return operation(session)
|
||||||
}()
|
}, sys.closeSSHConnection)
|
||||||
|
|
||||||
if opErr == nil {
|
if opErr == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -645,6 +653,43 @@ func (sys *System) runSSHOperation(timeout time.Duration, retries int, operation
|
|||||||
return fmt.Errorf("ssh operation failed")
|
return fmt.Errorf("ssh operation failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sshOperationTimeout bounds a single SSH data exchange (send request, read
|
||||||
|
// response, wait for the remote command to exit). It is more generous than the
|
||||||
|
// session-creation timeout to tolerate briefly slow agents, but is kept well
|
||||||
|
// under the collection interval so a stalled connection is detected and
|
||||||
|
// re-dialed within one cycle (see issue #2041).
|
||||||
|
const sshOperationTimeout = 20 * time.Second
|
||||||
|
|
||||||
|
// runWithTimeout runs op in a goroutine and returns its result, or, if op does
|
||||||
|
// not finish within timeout, calls onTimeout (used to tear down the connection
|
||||||
|
// so a blocked op can unwind) and returns a retryable timeout error. This
|
||||||
|
// guarantees the caller can never block indefinitely on a dead SSH connection.
|
||||||
|
func runWithTimeout(timeout time.Duration, op func() (bool, error), onTimeout func()) (retry bool, err error) {
|
||||||
|
type opResult struct {
|
||||||
|
retry bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
// Buffered so the op goroutine never leaks even when we return on timeout.
|
||||||
|
done := make(chan opResult, 1)
|
||||||
|
go func() {
|
||||||
|
r, e := op()
|
||||||
|
done <- opResult{retry: r, err: e}
|
||||||
|
}()
|
||||||
|
|
||||||
|
timer := time.NewTimer(timeout)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case res := <-done:
|
||||||
|
return res.retry, res.err
|
||||||
|
case <-timer.C:
|
||||||
|
if onTimeout != nil {
|
||||||
|
onTimeout()
|
||||||
|
}
|
||||||
|
return true, fmt.Errorf("ssh operation timed out after %s", timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// createSSHClient creates a new SSH client for the system
|
// createSSHClient creates a new SSH client for the system
|
||||||
func (s *System) createSSHClient() error {
|
func (s *System) createSSHClient() error {
|
||||||
if s.manager.sshConfig == nil {
|
if s.manager.sshConfig == nil {
|
||||||
@@ -660,7 +705,7 @@ func (s *System) createSSHClient() error {
|
|||||||
host = net.JoinHostPort(host, s.Port)
|
host = net.JoinHostPort(host, s.Port)
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
s.client, err = ssh.Dial(network, host, s.manager.sshConfig)
|
s.client, err = dialSSHWithKeepAlive(network, host, s.manager.sshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -669,6 +714,34 @@ func (s *System) createSSHClient() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sshKeepAliveInterval is the TCP keep-alive idle interval for SSH connections
|
||||||
|
// to agents. Enabling OS-level keep-alives lets the hub eventually detect a
|
||||||
|
// dead peer on an otherwise idle connection instead of trusting it forever.
|
||||||
|
// This is a backstop for genuine network death; an application-level wedge
|
||||||
|
// (agent process hung while its kernel keeps ACKing) is caught by the
|
||||||
|
// per-operation timeout in runSSHOperation instead (see issue #2041).
|
||||||
|
const sshKeepAliveInterval = 30 * time.Second
|
||||||
|
|
||||||
|
// dialSSHWithKeepAlive dials an SSH connection like ssh.Dial, but enables TCP
|
||||||
|
// keep-alive on the underlying connection so half-open connections are
|
||||||
|
// eventually detected by the operating system.
|
||||||
|
func dialSSHWithKeepAlive(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
|
||||||
|
dialer := net.Dialer{
|
||||||
|
Timeout: config.Timeout,
|
||||||
|
KeepAlive: sshKeepAliveInterval,
|
||||||
|
}
|
||||||
|
conn, err := dialer.Dial(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ssh.NewClient(sshConn, chans, reqs), nil
|
||||||
|
}
|
||||||
|
|
||||||
// createSessionWithTimeout creates a new SSH session with a timeout to avoid hanging
|
// createSessionWithTimeout creates a new SSH session with a timeout to avoid hanging
|
||||||
// in case of network issues
|
// in case of network issues
|
||||||
func (sys *System) createSessionWithTimeout(timeout time.Duration) (*ssh.Session, error) {
|
func (sys *System) createSessionWithTimeout(timeout time.Duration) (*ssh.Session, error) {
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ export function UserAuthForm({
|
|||||||
*/
|
*/
|
||||||
function redirectToOauthProvider(provider: AuthProviderInfo) {
|
function redirectToOauthProvider(provider: AuthProviderInfo) {
|
||||||
const url = new URL(provider.authURL)
|
const url = new URL(provider.authURL)
|
||||||
// url.searchParams.set("redirect_uri", `${window.location.origin}${basePath}`)
|
url.searchParams.set("redirect_uri", `${window.location.origin}${basePath}`)
|
||||||
sessionStorage.setItem("provider", JSON.stringify(provider))
|
localStorage.setItem("provider", JSON.stringify(provider))
|
||||||
window.location.href = url.toString()
|
window.location.href = url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +186,13 @@ export function UserAuthForm({
|
|||||||
const code = params.get("code")
|
const code = params.get("code")
|
||||||
if (code) {
|
if (code) {
|
||||||
const state = params.get("state")
|
const state = params.get("state")
|
||||||
const provider: AuthProviderInfo = JSON.parse(sessionStorage.getItem("provider") ?? "{}")
|
const provider: AuthProviderInfo = JSON.parse(localStorage.getItem("provider") ?? "{}")
|
||||||
|
localStorage.removeItem("provider")
|
||||||
|
window.history.replaceState({}, "", window.location.pathname)
|
||||||
if (!state || provider.state !== state) {
|
if (!state || provider.state !== state) {
|
||||||
showLoginFaliedToast()
|
showLoginFaliedToast()
|
||||||
} else {
|
} else {
|
||||||
setIsOauthLoading(true)
|
setIsOauthLoading(true)
|
||||||
window.history.replaceState({}, "", window.location.pathname)
|
|
||||||
pb.collection("users")
|
pb.collection("users")
|
||||||
.authWithOAuth2Code(provider.name, code, provider.codeVerifier, `${window.location.origin}${basePath}`)
|
.authWithOAuth2Code(provider.name, code, provider.codeVerifier, `${window.location.origin}${basePath}`)
|
||||||
.then(() => $authenticated.set(pb.authStore.isValid))
|
.then(() => $authenticated.set(pb.authStore.isValid))
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function ContainerMemoryChart({
|
|||||||
dataPoints={dataPoints}
|
dataPoints={dataPoints}
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(val, false, Unit.Bytes, true)
|
||||||
return `${toFixedFloat(value, val >= 10 ? 0 : 1)} ${unit}`
|
return `${toFixedFloat(value, value >= 10 ? 0 : 1)} ${unit}`
|
||||||
}}
|
}}
|
||||||
contentFormatter={(item) => {
|
contentFormatter={(item) => {
|
||||||
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
const { value, unit } = formatBytes(item.value, false, Unit.Bytes, true)
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
STATUS_COLORS.down
|
STATUS_COLORS.down
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
|
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full min-w-0">
|
||||||
<span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
|
<span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
|
||||||
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
|
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
|
||||||
<span className={meterClass} style={{ width: `${val}%` }}></span>
|
<span className={meterClass} style={{ width: `${val}%` }}></span>
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ const SystemCard = memo(
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm px-5 pt-3.5 pb-4">
|
<CardContent className="text-sm px-5 pt-3.5 pb-4">
|
||||||
<div className="grid gap-2.5" style={{ gridTemplateColumns: "24px minmax(80px, max-content) 1fr" }}>
|
<div className="grid gap-2.5" style={{ gridTemplateColumns: "24px minmax(80px, max-content) minmax(0, 1fr)" }}>
|
||||||
{table.getAllColumns().map((column) => {
|
{table.getAllColumns().map((column) => {
|
||||||
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
|
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
|
||||||
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
|
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
|
||||||
@@ -496,7 +496,7 @@ const SystemCard = memo(
|
|||||||
<div key={`${column.id}-label`} className="flex items-center text-muted-foreground pr-3">
|
<div key={`${column.id}-label`} className="flex items-center text-muted-foreground pr-3">
|
||||||
{name()}:
|
{name()}:
|
||||||
</div>
|
</div>
|
||||||
<div key={`${column.id}-value`} className="flex items-center">
|
<div key={`${column.id}-value`} className="flex items-center min-w-0">
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
2
internal/site/src/types.d.ts
vendored
2
internal/site/src/types.d.ts
vendored
@@ -208,7 +208,7 @@ interface ContainerStats {
|
|||||||
n: string
|
n: string
|
||||||
/** cpu percent */
|
/** cpu percent */
|
||||||
c: number
|
c: number
|
||||||
/** memory used (gb) */
|
/** memory used (mb) */
|
||||||
m: number
|
m: number
|
||||||
// network sent (mb)
|
// network sent (mb)
|
||||||
ns?: number
|
ns?: number
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
|
# Specialized images available for Nvidia / Intel GPUs
|
||||||
|
#
|
||||||
|
# Docs: https://beszel.dev/guide/agent-installation
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent' #Or henrygd/beszel-agent-nvidia
|
image: henrygd/beszel-agent
|
||||||
container_name: 'beszel-agent'
|
container_name: beszel-agent
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
# Only when using henrygd/beszel-agent-nvidia
|
|
||||||
# runtime: nvidia
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./beszel_agent_data:/var/lib/beszel-agent
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
# monitor other disks / partitions by mounting a folder in /extra-filesystems
|
||||||
# - /mnt/disk/.beszel:/extra-filesystems/sda1:ro
|
# - /mnt/disk1/.beszel:/extra-filesystems/disk1:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
LISTEN: 45876
|
||||||
KEY: 'ssh-ed25519 YOUR_PUBLIC_KEY'
|
KEY: "<public key>"
|
||||||
# Only when using henrygd/beszel-agent-nvidia
|
HUB_URL: "<hub url>"
|
||||||
# NVIDIA_VISIBLE_DEVICES: all
|
TOKEN: "<token>"
|
||||||
# NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/agent', 'health']
|
||||||
|
# interval: 120s
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
# Docs: https://beszel.dev/guide/hub-installation
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel:
|
beszel:
|
||||||
image: 'henrygd/beszel'
|
image: "henrygd/beszel"
|
||||||
container_name: 'beszel'
|
container_name: "beszel"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '8090:8090'
|
- "8090:8090"
|
||||||
volumes:
|
volumes:
|
||||||
- ./beszel_data:/beszel_data
|
- ./beszel_data:/beszel_data
|
||||||
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||||
|
# interval: 120s
|
||||||
|
# start_period: 10s
|
||||||
|
# timeout: 5s
|
||||||
|
|||||||
@@ -1,26 +1,38 @@
|
|||||||
|
# Docs: https://beszel.dev/guide/getting-started
|
||||||
|
# Env vars: https://beszel.dev/guide/environment-variables
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel:
|
beszel:
|
||||||
image: 'henrygd/beszel'
|
image: henrygd/beszel:latest
|
||||||
container_name: 'beszel'
|
container_name: beszel
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
APP_URL: http://localhost:8090
|
||||||
ports:
|
ports:
|
||||||
- '8090:8090'
|
- 8090:8090
|
||||||
volumes:
|
volumes:
|
||||||
- ./beszel_data:/beszel_data
|
- ./beszel_data:/beszel_data
|
||||||
extra_hosts:
|
- ./beszel_socket:/beszel_socket
|
||||||
- 'host.docker.internal:host-gateway'
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||||
|
# interval: 120s
|
||||||
|
# start_period: 10s
|
||||||
|
# timeout: 5s
|
||||||
|
|
||||||
beszel-agent:
|
beszel-agent:
|
||||||
image: 'henrygd/beszel-agent' #Add -nvidia for nvidia gpus
|
image: henrygd/beszel-agent:latest
|
||||||
container_name: 'beszel-agent'
|
container_name: beszel-agent
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
# runtime: nvidia # when using beszel-agent-nvidia
|
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./beszel_agent_data:/var/lib/beszel-agent
|
||||||
|
- ./beszel_socket:/beszel_socket
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
environment:
|
environment:
|
||||||
PORT: 45876
|
LISTEN: /beszel_socket/beszel.sock
|
||||||
KEY: '...'
|
HUB_URL: http://localhost:8090
|
||||||
# FILESYSTEM: /dev/sda1 # set to the correct filesystem for disk I/O stats
|
TOKEN: <token>
|
||||||
# NVIDIA_VISIBLE_DEVICES: all # when using beszel-agent-nvidia
|
KEY: "<key>"
|
||||||
# NVIDIA_DRIVER_CAPABILITIES: utility # when using beszel-agent-nvidia
|
# healthcheck:
|
||||||
|
# test: ['CMD', '/agent', 'health']
|
||||||
|
# interval: 120s
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ is_opnsense() {
|
|||||||
[ -f /usr/local/sbin/opnsense-version ] || [ -f /usr/local/etc/opnsense-version ] || [ -f /etc/opnsense-release ]
|
[ -f /usr/local/sbin/opnsense-version ] || [ -f /usr/local/etc/opnsense-version ] || [ -f /etc/opnsense-release ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_pfsense() {
|
||||||
|
[ -f /etc/rc.bootup ] && grep -qi "pfSense" /etc/rc.bootup
|
||||||
|
}
|
||||||
|
|
||||||
is_glibc() {
|
is_glibc() {
|
||||||
# Prefer glibc-enabled agent (NVML via purego) on linux/amd64 glibc systems.
|
# Prefer glibc-enabled agent (NVML via purego) on linux/amd64 glibc systems.
|
||||||
# Check common dynamic loader paths first (fast + reliable).
|
# Check common dynamic loader paths first (fast + reliable).
|
||||||
@@ -183,6 +187,18 @@ run_rc_command "$1"
|
|||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Generate the pfSense boot hook. pfSense only executes *.sh files in
|
||||||
|
# /usr/local/etc/rc.d and may run them again during network events.
|
||||||
|
generate_pfsense_boot_script() {
|
||||||
|
cat <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if ! /usr/sbin/service beszel-agent status >/dev/null 2>&1; then
|
||||||
|
/usr/sbin/service beszel-agent onestart
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
# Detect system architecture
|
# Detect system architecture
|
||||||
detect_architecture() {
|
detect_architecture() {
|
||||||
local arch=$(uname -m)
|
local arch=$(uname -m)
|
||||||
@@ -437,6 +453,7 @@ if [ "$UNINSTALL" = true ]; then
|
|||||||
|
|
||||||
echo "Removing the FreeBSD service files..."
|
echo "Removing the FreeBSD service files..."
|
||||||
rm -f /usr/local/etc/rc.d/beszel-agent
|
rm -f /usr/local/etc/rc.d/beszel-agent
|
||||||
|
rm -f /usr/local/etc/rc.d/beszel-agent-start.sh
|
||||||
|
|
||||||
# Remove the daily update cron job if it exists
|
# Remove the daily update cron job if it exists
|
||||||
echo "Removing the daily update cron job..."
|
echo "Removing the daily update cron job..."
|
||||||
@@ -595,8 +612,8 @@ elif is_openwrt; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
elif is_freebsd; then
|
elif is_freebsd; then
|
||||||
if is_opnsense; then
|
if is_opnsense || is_pfsense; then
|
||||||
echo "OPNsense detected: skipping user creation (using daemon user instead)"
|
echo "Firewall appliance detected: skipping user creation (using daemon user instead)"
|
||||||
AGENT_USER="daemon"
|
AGENT_USER="daemon"
|
||||||
else
|
else
|
||||||
if ! id -u beszel >/dev/null 2>&1; then
|
if ! id -u beszel >/dev/null 2>&1; then
|
||||||
@@ -908,11 +925,11 @@ KEY="$KEY"
|
|||||||
TOKEN=$TOKEN
|
TOKEN=$TOKEN
|
||||||
HUB_URL=$HUB_URL
|
HUB_URL=$HUB_URL
|
||||||
EOF
|
EOF
|
||||||
chmod 640 "$AGENT_DIR/env"
|
|
||||||
chown "root:${AGENT_USER}" "$AGENT_DIR/env"
|
|
||||||
else
|
else
|
||||||
echo "FreeBSD environment file already exists. Skipping creation."
|
echo "FreeBSD environment file already exists. Skipping creation."
|
||||||
fi
|
fi
|
||||||
|
chmod 640 "$AGENT_DIR/env"
|
||||||
|
chown "root:${AGENT_USER}" "$AGENT_DIR/env"
|
||||||
|
|
||||||
# Create the rc service file if it doesn't exist
|
# Create the rc service file if it doesn't exist
|
||||||
if [ ! -f /usr/local/etc/rc.d/beszel-agent ]; then
|
if [ ! -f /usr/local/etc/rc.d/beszel-agent ]; then
|
||||||
@@ -924,6 +941,12 @@ EOF
|
|||||||
echo "FreeBSD rc service file already exists. Skipping creation."
|
echo "FreeBSD rc service file already exists. Skipping creation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if is_pfsense; then
|
||||||
|
echo "Creating pfSense boot script..."
|
||||||
|
generate_pfsense_boot_script > /usr/local/etc/rc.d/beszel-agent-start.sh
|
||||||
|
chmod 755 /usr/local/etc/rc.d/beszel-agent-start.sh
|
||||||
|
fi
|
||||||
|
|
||||||
# Enable and start the service
|
# Enable and start the service
|
||||||
echo "Enabling and starting the agent service..."
|
echo "Enabling and starting the agent service..."
|
||||||
sysrc beszel_agent_enable="YES"
|
sysrc beszel_agent_enable="YES"
|
||||||
|
|||||||
Reference in New Issue
Block a user