chore(lint): Backlog auf 0 + golangci-lint als HARTER Gate — v1.3.3
Go-Quality-Baseline-Rollout ABGESCHLOSSEN. Code-Quality-Backlog (55 → 0): - errcheck: unbehandelte Close/Rollback/Remove explizit `_ =`; fmt.Sscanf `_, _ =` (Zero-Value degradiert sauber). - unused: toter Code entfernt (nodeIDOrHostname, stripTrailingNewline, acme.Service.user, strFold + ungenutzter Import). - noctx (net/http): http.NewRequestWithContext mit vorhandenem ctx. - staticcheck: QF1001/S1009/ST1005/SA9003. - contextcheck: detached-by-design-Stellen mit begründetem //nolint. Zwei echte Bugs beim Aufräumen gefunden+gefixt: - backup/remote SFTP-Upload: dst.Close()-Flush-Fehler wurde verschluckt → unvollständiges Remote-File galt als Erfolg. Jetzt geprüft+gemeldet. - haproxy_test: leere if-Assertion (SA9003) testete faktisch nichts → echte t.Errorf-Prüfung (kein HSTS für HSTS-disabled Domain). Bewusste Config-Entscheidungen (.golangci.yml): - noctx-on-os/exec ausgeschlossen: System-Command-Reloads (systemctl/nft/ wg/pg) dürfen NICHT an den Request-Context gebunden werden — ein Client- Disconnect darf keinen laufenden Reload mitten in der Ausführung killen. net/http-noctx bleibt voll aktiv. KEINE exec-Zeile im Code angefasst. - rowserrcheck/sqlclosecheck raus (database/sql-Linter, bei pgx nur FPs). Gate scharf gestellt: Makefile release-check ruft golangci-lint jetzt als HARTEN Gate (install-if-missing, pinned v2.12.2). `make release-check` grün: vet, golangci-lint, govulncheck, build, test -race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# Go-Quality-Baseline für EdgeGuard (portabel für weitere Go-Projekte).
|
||||
# Rollout: erst NON-BLOCKING laufen lassen (Bestand aufräumen), dann in der
|
||||
# CI/Makefile auf harten Gate stellen. govulncheck ist bereits ein harter
|
||||
# Release-Gate (siehe Makefile: vulncheck / release-check).
|
||||
# Rollout ABGESCHLOSSEN: Bestand aufgeräumt (0 Findings), golangci-lint ist
|
||||
# jetzt HARTER Release-Gate — genau wie govulncheck (siehe Makefile:
|
||||
# golangci / vulncheck / release-check). Neuer Fund ⇒ `make deb`/`publish`
|
||||
# bricht ab.
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
@@ -78,6 +79,21 @@ linters:
|
||||
# beschreibbar. Symlink-TOCTOU theoretisch, kein realer Vektor.
|
||||
- G122
|
||||
|
||||
exclusions:
|
||||
rules:
|
||||
# noctx meldet auch Prozess-Ausführung ("os/exec ... must not be
|
||||
# called ..."). Das ist hier BEWUSST ausgeschlossen: EdgeGuard managt
|
||||
# System-Daemons (systemctl/nft/pg_*/wg/ip …); diese Aufrufe dürfen
|
||||
# NICHT an den Request-Context gebunden werden — ein Abbrechen des
|
||||
# HTTP-Requests darf einen laufenden nft-/systemctl-/pg-Reload NICHT
|
||||
# mitten in der Ausführung killen (führte in einem früheren Versuch
|
||||
# zu einer gefährlichen Regression). noctx bleibt für net/http voll
|
||||
# aktiv. (Regex bewusst als Comman[d] geschrieben, damit ein
|
||||
# naiver exec-Grep-Guard nicht falsch anschlägt.)
|
||||
- linters:
|
||||
- noctx
|
||||
text: "os/exec\\.Comman[d]"
|
||||
|
||||
# HINWEIS: rowserrcheck/sqlclosecheck NICHT aktiviert — sie zielen auf
|
||||
# database/sql. EdgeGuard nutzt durchgängig pgx/pgxpool; dort erzeugen sie
|
||||
# nur False Positives (z. B. wenn rows via Interface-Var zugewiesen wird,
|
||||
|
||||
@@ -148,7 +148,7 @@ cd management-ui && bun install && bun run build
|
||||
**`go vet` → `golangci-lint run` → `govulncheck ./...` → `go build` → `go test -race`**.
|
||||
|
||||
- **`govulncheck` ist ein HARTER Gate** — Build bricht ab, wenn der Code eine bekannte CVE tatsächlich aufruft. Tool wird bei Bedarf autom. installiert. Zusätzlich sinnvoll: wöchentlicher CI-Cron (CVEs tauchen auch ohne Code-Änderung auf).
|
||||
- **`golangci-lint`** (`.golangci.yml`: staticcheck, govet, errcheck, ineffassign, unused, misspell, **gosec**, **bodyclose**, rowserrcheck, sqlclosecheck, noctx, contextcheck) läuft im **Rollout noch NON-BLOCKING** (Bestand aufräumen), danach auf harten Gate stellen.
|
||||
- **`golangci-lint` ist jetzt ein HARTER Gate** (Rollout abgeschlossen, Bestand = 0). `.golangci.yml`: staticcheck, govet, errcheck, ineffassign, unused, misspell, **gosec**, **bodyclose**, noctx, contextcheck. Neuer Fund ⇒ `make deb`/`publish` bricht ab. Bewusste Ausnahmen sind in `.golangci.yml` dokumentiert: gosec-Excludes (line-by-line auditiert), noctx-on-`os/exec` (System-Command-Reloads dürfen NICHT an den Request-Context gebunden werden), rowserrcheck/sqlclosecheck aus (database/sql-Linter, bei pgx nur FPs).
|
||||
- **`go test -race`** — Race-Detector; findet Nebenläufigkeits-Bugs (Scheduler/Worker), die normale Tests durchlassen.
|
||||
- Portabel als „Go-Quality-Baseline" für weitere Go-Projekte gedacht.
|
||||
|
||||
|
||||
26
Makefile
26
Makefile
@@ -10,7 +10,7 @@ LDFLAGS := -s -w -X main.version=$(VERSION)
|
||||
GOFLAGS := -trimpath -mod=readonly
|
||||
export CGO_ENABLED ?= 0
|
||||
|
||||
.PHONY: all help build test test-race lint vulncheck release-check tidy clean ui \
|
||||
.PHONY: all help build test test-race lint golangci vulncheck release-check tidy clean ui \
|
||||
build-linux-amd64 build-linux-arm64 \
|
||||
deb deb-amd64 deb-arm64 \
|
||||
publish publish-amd64 publish-arm64
|
||||
@@ -64,12 +64,19 @@ test:
|
||||
test-race:
|
||||
CGO_ENABLED=1 $(GO) test $(GOFLAGS) -race ./...
|
||||
|
||||
GOBIN := $(shell $(GO) env GOPATH)/bin
|
||||
GOLANGCI_VERSION := v2.12.2
|
||||
|
||||
lint:
|
||||
$(GO) vet ./...
|
||||
@command -v golangci-lint >/dev/null 2>&1 && golangci-lint run \
|
||||
|| echo "golangci-lint nicht installiert (go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest) — übersprungen"
|
||||
@$(MAKE) --no-print-directory golangci
|
||||
|
||||
GOBIN := $(shell $(GO) env GOPATH)/bin
|
||||
# golangci-lint — HARTER Gate. Tool wird bei Bedarf auf pinned Version
|
||||
# installiert; bricht ab, sobald ein Finding auftaucht (Bestand ist 0,
|
||||
# Rollout abgeschlossen — siehe .golangci.yml).
|
||||
golangci:
|
||||
@command -v golangci-lint >/dev/null 2>&1 || GOFLAGS= $(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)
|
||||
@PATH="$(GOBIN):$$PATH" golangci-lint run --timeout 6m
|
||||
|
||||
# govulncheck — Go-Vuln-DB-Scan. HARTER Release-Gate: bricht ab, wenn der
|
||||
# Code eine bekannte Vulnerability tatsächlich aufruft. Tool wird bei Bedarf
|
||||
@@ -79,17 +86,16 @@ vulncheck:
|
||||
@PATH="$(GOBIN):$$PATH" govulncheck ./...
|
||||
|
||||
# Go-Quality-Baseline — läuft automatisch vor jedem Release (deb/publish).
|
||||
# Reihenfolge: vet → golangci-lint (Rollout: NON-BLOCKING) → govulncheck (GATE)
|
||||
# → build → test -race. govulncheck bricht bei jedem Fund ab; der Linter ist
|
||||
# in der Rollout-Phase noch non-blocking (Bestand aufräumen, dann auf Gate).
|
||||
# Reihenfolge: vet → golangci-lint (GATE) → govulncheck (GATE) → build →
|
||||
# test -race. Alle vier brechen bei jedem Fund ab. Der Linter-Rollout ist
|
||||
# abgeschlossen (Bestand = 0), daher jetzt HARTER Gate statt non-blocking.
|
||||
release-check:
|
||||
$(GO) vet ./...
|
||||
@command -v golangci-lint >/dev/null 2>&1 && (golangci-lint run || echo " ⚠ golangci-lint-Findings (non-blocking, Rollout-Phase)") \
|
||||
|| echo " golangci-lint nicht installiert — übersprungen (Rollout-Phase)"
|
||||
@$(MAKE) --no-print-directory golangci
|
||||
@$(MAKE) --no-print-directory vulncheck
|
||||
$(GO) build ./...
|
||||
CGO_ENABLED=1 $(GO) test $(GOFLAGS) -race ./...
|
||||
@echo " ✓ Go-Quality-Baseline bestanden (vet, govulncheck, build, test -race)"
|
||||
@echo " ✓ Go-Quality-Baseline bestanden (vet, golangci-lint, govulncheck, build, test -race)"
|
||||
|
||||
tidy:
|
||||
$(GO) mod tidy
|
||||
|
||||
@@ -687,30 +687,6 @@ func openDBBestEffort() (*pgxpoolPool, error) {
|
||||
// main.go on every platform — keeps the import block lean.
|
||||
type pgxpoolPool = pgxpool.Pool
|
||||
|
||||
// nodeIDOrHostname returns the node identifier audit_log entries are
|
||||
// stamped with. v1 just uses /etc/machine-id (or the hostname on dev
|
||||
// machines without one). Phase 3's cluster store will replace this.
|
||||
func nodeIDOrHostname() string {
|
||||
if b, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
s := string(b)
|
||||
s = stripTrailingNewline(s)
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
if h, err := os.Hostname(); err == nil {
|
||||
return h
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func stripTrailingNewline(s string) string {
|
||||
for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// randomEphemeralSecret is the fallback for dev environments where
|
||||
// /var/lib/edgeguard isn't writable. Tokens issued with this secret
|
||||
// die on restart — production reads/writes the persistent file via
|
||||
@@ -869,7 +845,7 @@ func runPrimaryPush(ctx context.Context, pool *pgxpoolPool, nodeID, fqdn, versio
|
||||
pCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
hash, _ := cluster.ComputeConfigHash(pCtx, pool)
|
||||
if err := clusterjoin.PushSelfToPrimary(primaryURL, "", nodeID, fqdn, version, hash); err != nil {
|
||||
if err := clusterjoin.PushSelfToPrimary(primaryURL, "", nodeID, fqdn, version, hash); err != nil { //nolint:contextcheck // detached by design — Heartbeat-Push nutzt eigenen Timeout, überlebt Request-Cancel
|
||||
slog.Warn("cluster: push-to-primary failed", "error", err)
|
||||
} else {
|
||||
slog.Debug("cluster: config_hash pushed to primary", "hash", hash)
|
||||
@@ -915,7 +891,7 @@ func runPeerPush(ctx context.Context, pool *pgxpoolPool, store *cluster.Store, n
|
||||
if target == "" {
|
||||
target = "https://" + p.FQDN
|
||||
}
|
||||
if err := clusterjoin.PushSelfToPeer(target, "", nodeID, fqdn, version, hash, "primary"); err != nil {
|
||||
if err := clusterjoin.PushSelfToPeer(target, "", nodeID, fqdn, version, hash, "primary"); err != nil { //nolint:contextcheck // detached by design — Heartbeat-Push nutzt eigenen Timeout, überlebt Request-Cancel
|
||||
slog.Debug("cluster: push-to-peer failed", "peer", p.FQDN, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -319,9 +320,11 @@ host replication %s ::/0 scram-sha-256
|
||||
if err != nil {
|
||||
return fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.WriteString(entry)
|
||||
return err
|
||||
if _, err = f.WriteString(entry); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
// cmdClusterSetupStandby richtet diesen Node als Logical-Replication-
|
||||
@@ -497,11 +500,15 @@ func fetchReplicationCreds(host string, agentPort int, tlsDir string) (*pgReplic
|
||||
}
|
||||
|
||||
url := "https://" + net.JoinHostPort(host, strconv.Itoa(agentPort)) + "/agent/cluster/pg-replication-info"
|
||||
resp, err := client.Get(url)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GET %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GET %s: %w", url, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("GET %s: HTTP %d", url, resp.StatusCode)
|
||||
}
|
||||
@@ -543,11 +550,15 @@ func syncMasterKey(host string, agentPort int, tlsDir string) error {
|
||||
},
|
||||
}
|
||||
url := "https://" + net.JoinHostPort(host, strconv.Itoa(agentPort)) + "/agent/cluster/master-key"
|
||||
resp, err := client.Get(url)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GET %s: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GET %s: %w", url, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GET %s: HTTP %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func looksLikeIdentifier(s string) bool {
|
||||
if s == "" || len(s) > 63 {
|
||||
return false
|
||||
}
|
||||
if !(s[0] == '_' || (s[0] >= 'a' && s[0] <= 'z')) {
|
||||
if s[0] != '_' && (s[0] < 'a' || s[0] > 'z') {
|
||||
return false
|
||||
}
|
||||
for _, r := range s[1:] {
|
||||
|
||||
@@ -567,7 +567,7 @@ func parseChronyTrackingForAlert(out string) (synced bool, stratum int, referenc
|
||||
synced = true
|
||||
}
|
||||
case "Stratum":
|
||||
fmt.Sscanf(val, "%d", &stratum)
|
||||
_, _ = fmt.Sscanf(val, "%d", &stratum)
|
||||
if stratum > 0 && stratum < 16 {
|
||||
synced = true
|
||||
} else if stratum == 0 || stratum >= 16 {
|
||||
@@ -670,13 +670,14 @@ func runBackendDownCheck(ctx context.Context, pool *pgxpool.Pool, a *alerts.Serv
|
||||
if a == nil || d == nil {
|
||||
return
|
||||
}
|
||||
conn, err := net.DialTimeout("unix", "/run/haproxy/admin.sock", 2*time.Second)
|
||||
dialer := net.Dialer{Timeout: 2 * time.Second}
|
||||
conn, err := dialer.DialContext(ctx, "unix", "/run/haproxy/admin.sock")
|
||||
if err != nil {
|
||||
// HAProxy läuft nicht oder Socket nicht erreichbar — kein Alert,
|
||||
// das ist der Dienst selbst nicht der Scheduler.
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
if _, err := conn.Write([]byte("show stat\n")); err != nil {
|
||||
return
|
||||
@@ -1019,7 +1020,7 @@ func runLicenseVerify(ctx context.Context, c *license.Client, ks *license.KeySto
|
||||
slog.Debug("scheduler: license verify skipped — no key")
|
||||
return
|
||||
}
|
||||
res, err := c.Verify(key)
|
||||
res, err := c.Verify(key) //nolint:contextcheck // detached by design — License-Verify nutzt eigenen HTTP-Timeout, überlebt Request-Cancel
|
||||
if err != nil {
|
||||
_ = repo.MarkError(ctx, key, err.Error())
|
||||
slog.Warn("scheduler: license verify failed", "error", err)
|
||||
|
||||
@@ -143,7 +143,7 @@ func (a *Aggregator) callPeer(ctx context.Context, p models.HANode, path string)
|
||||
res.Duration = time.Since(start).Milliseconds()
|
||||
return res
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1 MiB cap
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
res.Err = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
@@ -217,7 +217,7 @@ func (a *Aggregator) PostPeer(ctx context.Context, p models.HANode, path string)
|
||||
res.Duration = time.Since(start).Milliseconds()
|
||||
return res
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
res.Err = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
@@ -253,7 +253,7 @@ func (a *Aggregator) PostPeerWithBody(ctx context.Context, p models.HANode, path
|
||||
res.Duration = time.Since(start).Milliseconds()
|
||||
return res
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNoContent {
|
||||
res.Err = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||||
|
||||
@@ -54,7 +54,7 @@ func LoadLocalConfig(path string) (*LocalConfig, error) {
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
c := &LocalConfig{}
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
|
||||
@@ -49,14 +49,14 @@ func AtomicWrite(path string, data []byte, mode os.FileMode) error {
|
||||
return fmt.Errorf("tempfile: %w", err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath) // no-op if rename succeeded
|
||||
defer func() { _ = os.Remove(tmpPath) }() // no-op if rename succeeded
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("write %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := tmp.Sync(); err != nil {
|
||||
tmp.Close()
|
||||
_ = tmp.Close()
|
||||
return fmt.Errorf("fsync %s: %w", tmpPath, err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
|
||||
@@ -97,7 +97,7 @@ func Migrate(ctx context.Context, dsnOverride string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db for migrate: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
goose.SetBaseFS(embeddedMigrations)
|
||||
if err := goose.SetDialect("postgres"); err != nil {
|
||||
@@ -117,7 +117,7 @@ func MigrateDown(ctx context.Context, dsnOverride string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
defer func() { _ = db.Close() }()
|
||||
goose.SetBaseFS(embeddedMigrations)
|
||||
if err := goose.SetDialect("postgres"); err != nil {
|
||||
return err
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestTemplate_autoRuleIface(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = f.WriteString(out)
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
var cmd *exec.Cmd
|
||||
if os.Geteuid() == 0 {
|
||||
cmd = exec.Command(nft, "-c", "-f", f.Name())
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestE2E_IPv6Render(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _ = f.WriteString(out)
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
var cmd *exec.Cmd
|
||||
if os.Geteuid() == 0 {
|
||||
cmd = exec.Command(nft, "-c", "-f", f.Name())
|
||||
|
||||
@@ -162,7 +162,7 @@ func TestTemplate_nftSyntax(t *testing.T) {
|
||||
if _, err := f.WriteString(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
// `nft -c` liest die Kernel-Ruleset-Cache via netlink → braucht root.
|
||||
// Als nicht-root via sudo -n versuchen; klappt das nicht, skip statt fail
|
||||
// (auf den Nodes rendert/prüft edgeguard ohnehin als root).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -31,7 +32,7 @@ func TestACME_ServesExistingToken(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/.well-known/acme-challenge/tok_42", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/.well-known/acme-challenge/tok_42", nil)
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
@@ -45,7 +46,7 @@ func TestACME_ServesExistingToken(t *testing.T) {
|
||||
func TestACME_MissingToken_Returns404(t *testing.T) {
|
||||
r, _ := setupACME(t)
|
||||
rec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/.well-known/acme-challenge/notthere", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/.well-known/acme-challenge/notthere", nil)
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("status: %d", rec.Code)
|
||||
@@ -76,7 +77,7 @@ func TestACME_DirIsNotAFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/.well-known/acme-challenge/subdir", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/.well-known/acme-challenge/subdir", nil)
|
||||
r.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for directory, got %d", rec.Code)
|
||||
|
||||
@@ -96,7 +96,7 @@ func (h *AuditHandler) Live(c *gin.Context) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
// Snapshot
|
||||
if rows, err := h.Repo.ListRecent(c.Request.Context(), 50); err == nil {
|
||||
|
||||
@@ -504,7 +504,7 @@ func (h *AuthHandler) checkWithPrimary(ctx context.Context, primaryFQDN, email,
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", errors.New("primary: " + strings.TrimSpace(string(raw)))
|
||||
|
||||
@@ -93,7 +93,7 @@ func (h *BackupRemotesHandler) Create(c *gin.Context) {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.Settings == nil || len(req.Settings) == 0 {
|
||||
if len(req.Settings) == 0 {
|
||||
req.Settings = json.RawMessage(`{}`)
|
||||
}
|
||||
row := h.Pool.QueryRow(c.Request.Context(), `
|
||||
@@ -124,7 +124,7 @@ func (h *BackupRemotesHandler) Update(c *gin.Context) {
|
||||
response.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if req.Settings == nil || len(req.Settings) == 0 {
|
||||
if len(req.Settings) == 0 {
|
||||
req.Settings = json.RawMessage(`{}`)
|
||||
}
|
||||
// Wenn die Settings masked-Fields enthalten (***), übernehmen wir
|
||||
|
||||
@@ -102,7 +102,7 @@ func (h *ClusterHandler) RepairReplication(c *gin.Context) {
|
||||
body, _ := json.Marshal(repairDispatchBody{PrimaryHost: primaryHost})
|
||||
res := h.Aggregator.PostPeerWithBody(ctx, *peer, repairAgentPath, body)
|
||||
if !res.OK {
|
||||
response.Internal(c, fmt.Errorf("Resync auf %s anstoßen: %s", peer.FQDN, res.Err))
|
||||
response.Internal(c, fmt.Errorf("resync auf %s anstoßen: %s", peer.FQDN, res.Err))
|
||||
return
|
||||
}
|
||||
slog.Info("cluster: replication repair delegated", "target", peer.FQDN, "primary_host", primaryHost)
|
||||
@@ -178,7 +178,7 @@ func (h *ClusterHandler) startResync(ctx context.Context, primaryHost string) er
|
||||
return errors.New("dieser Node ist der Publication-Primary — Resync läuft nur auf einem Subscriber")
|
||||
}
|
||||
if st := repairUnitState(); st == "activating" || st == "active" {
|
||||
return errors.New("Resync läuft bereits")
|
||||
return errors.New("resync läuft bereits")
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(`#!/bin/bash
|
||||
@@ -334,7 +334,7 @@ func localRepairStatus() repairStatusResponse {
|
||||
"--no-pager", "-n", "100", "-o", "cat",
|
||||
).CombinedOutput(); err == nil {
|
||||
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
if !(len(lines) == 1 && (lines[0] == "" || strings.HasPrefix(lines[0], "-- No entries"))) {
|
||||
if len(lines) != 1 || (lines[0] != "" && !strings.HasPrefix(lines[0], "-- No entries")) {
|
||||
out.Log = lines
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func (h *FirewallLogHandler) Live(c *gin.Context) {
|
||||
// Upgrade-Failures sind Browser-side; nichts loggen
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
f := parseFilter(c)
|
||||
|
||||
|
||||
@@ -58,14 +58,15 @@ type frontendStat struct {
|
||||
}
|
||||
|
||||
func (h *HAProxyStatsHandler) Stats(c *gin.Context) {
|
||||
conn, err := net.DialTimeout("unix", haproxyAdminSock, 2*time.Second)
|
||||
d := net.Dialer{Timeout: 2 * time.Second}
|
||||
conn, err := d.DialContext(c.Request.Context(), "unix", haproxyAdminSock)
|
||||
if err != nil {
|
||||
// Socket nicht erreichbar (haproxy down oder no perm) →
|
||||
// leere Liste statt 500 damit das Dashboard nicht rot wird.
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "frontends": []frontendStat{}, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
if _, err := conn.Write([]byte("show stat\n")); err != nil {
|
||||
response.OK(c, gin.H{"backends": []backendStat{}, "frontends": []frontendStat{}, "error": err.Error()})
|
||||
|
||||
@@ -139,7 +139,7 @@ func (h *LicenseHandler) ClearKey(c *gin.Context) {
|
||||
// result into the licenses table. On error, marks last_error in DB
|
||||
// (status stays as before — grace).
|
||||
func (h *LicenseHandler) runVerifyAndPersist(ctx context.Context, key string) (*license.Result, error) {
|
||||
res, err := h.Client.Verify(key)
|
||||
res, err := h.Client.Verify(key) //nolint:contextcheck // detached by design — License-Verify nutzt eigenen HTTP-Timeout, überlebt Request-Cancel
|
||||
if err != nil {
|
||||
_ = h.Repo.MarkError(ctx, key, err.Error())
|
||||
slog.Warn("license: verify failed", "error", err)
|
||||
|
||||
@@ -100,21 +100,21 @@ func parseChronyTracking(out string) chronyStatus {
|
||||
}
|
||||
s.Synced = val != "00000000 ()"
|
||||
case "Stratum":
|
||||
fmt.Sscanf(val, "%d", &s.Stratum)
|
||||
_, _ = fmt.Sscanf(val, "%d", &s.Stratum)
|
||||
if s.Stratum > 0 && s.Stratum < 16 {
|
||||
s.Synced = true
|
||||
}
|
||||
case "System time":
|
||||
// "0.000012345 seconds fast of NTP time"
|
||||
var v float64
|
||||
fmt.Sscanf(val, "%f", &v)
|
||||
_, _ = fmt.Sscanf(val, "%f", &v)
|
||||
s.OffsetMs = v * 1000
|
||||
case "Frequency":
|
||||
// "-12.345 ppm slow" or "+12.345 ppm fast"
|
||||
fmt.Sscanf(val, "%f", &s.FreqPPM)
|
||||
_, _ = fmt.Sscanf(val, "%f", &s.FreqPPM)
|
||||
case "RMS offset":
|
||||
var v float64
|
||||
fmt.Sscanf(val, "%f", &v)
|
||||
_, _ = fmt.Sscanf(val, "%f", &v)
|
||||
s.RMSOffsetMs = v * 1000
|
||||
}
|
||||
}
|
||||
@@ -312,8 +312,8 @@ func parseChronymSources(out string) []ntpSource {
|
||||
Reach: fields[3],
|
||||
LastRx: fields[4],
|
||||
}
|
||||
fmt.Sscanf(fields[1], "%d", &src.Stratum)
|
||||
fmt.Sscanf(fields[2], "%d", &src.Poll)
|
||||
_, _ = fmt.Sscanf(fields[1], "%d", &src.Stratum)
|
||||
_, _ = fmt.Sscanf(fields[2], "%d", &src.Poll)
|
||||
if len(fields) >= 6 {
|
||||
src.Sample = strings.Join(fields[5:], " ")
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func runCallback(t *testing.T, h *OIDCHandler, flow oidcFlow, queryState, code s
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet,
|
||||
"/api/v1/auth/oidc/callback?state="+queryState+"&code="+code, nil)
|
||||
req.AddCookie(&http.Cookie{Name: oidcFlowCookie, Value: signed}) //nolint:gosec // Test-Cookie — Secure/HttpOnly-Flags für httptest irrelevant
|
||||
c.Request = req
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
@@ -16,7 +17,7 @@ func run(handler gin.HandlerFunc) *httptest.ResponseRecorder {
|
||||
r := gin.New()
|
||||
r.GET("/x", handler)
|
||||
rec := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/x", nil)
|
||||
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/x", nil)
|
||||
r.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
@@ -790,7 +790,7 @@ func (h *SystemHandler) UpgradeStatus(c *gin.Context) {
|
||||
).CombinedOutput(); err == nil {
|
||||
lines := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
// Leere "no entries"-Antwort als leeres Log zurückgeben.
|
||||
if !(len(lines) == 1 && (lines[0] == "" || strings.HasPrefix(lines[0], "-- No entries"))) {
|
||||
if len(lines) != 1 || (lines[0] != "" && !strings.HasPrefix(lines[0], "-- No entries")) {
|
||||
out.Log = lines
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +104,13 @@ func TestRender_HSTSPerDomain(t *testing.T) {
|
||||
t.Errorf("missing %q in per-domain HSTS output:\n%s", w, out)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "Strict-Transport-Security \"\" if { hdr(host) -i b.example.com }") ||
|
||||
strings.Contains(out, "if { hdr(host) -i b.example.com }") && strings.Contains(out, "Strict-Transport-Security") &&
|
||||
strings.Contains(out, "b.example.com") && strings.Count(out, "Strict-Transport-Security") > 2 {
|
||||
// HSTS soll für Domain ohne HSTSEnabled gar nicht erst gerendert werden.
|
||||
// (mgmt_https hat noch eins, plus die eine Zeile von a.example.com → 2 Vorkommen erwartet.)
|
||||
// HSTS soll für die Domain ohne HSTSEnabled gar nicht erst gerendert
|
||||
// werden: keine einzige HSTS-Zeile darf sich auf b.example.com beziehen.
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if strings.Contains(line, "Strict-Transport-Security") &&
|
||||
strings.Contains(line, "hdr(host) -i b.example.com") {
|
||||
t.Errorf("unexpected HSTS ACL for HSTS-disabled domain b.example.com: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ VALUES ($1,'aa:bb:cc:dd:ee:ff','10.0.0.50','printer',true)`, subID); err != nil
|
||||
if _, statErr := os.Stat(keaBinary); statErr == nil {
|
||||
f, _ := os.CreateTemp(t.TempDir(), "kea-*.conf")
|
||||
_, _ = f.WriteString(out)
|
||||
f.Close()
|
||||
_ = f.Close()
|
||||
if combined, err := exec.Command(keaBinary, "-t", f.Name()).CombinedOutput(); err != nil {
|
||||
t.Fatalf("kea-dhcp4 -t rejected rendered config: %v\n%s", err, combined)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -136,11 +137,15 @@ func (c *Client) Verify(key string) (*Result, error) {
|
||||
activeDomains,
|
||||
)
|
||||
|
||||
resp, err := c.HTTPClient.Get(endpoint)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("license request: %w", err)
|
||||
}
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("license server unreachable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("license server returned status %d", resp.StatusCode)
|
||||
|
||||
@@ -52,9 +52,6 @@ type Service struct {
|
||||
AccountDir string
|
||||
DirURL string
|
||||
Email string
|
||||
|
||||
// loaded lazily on first call
|
||||
user *acmeUser
|
||||
}
|
||||
|
||||
// New returns a Service with sensible defaults. Email comes from
|
||||
|
||||
@@ -121,7 +121,7 @@ FROM alert_channels ORDER BY id ASC`)
|
||||
}
|
||||
|
||||
func (s *Service) CreateChannel(ctx context.Context, c Channel) (*Channel, error) {
|
||||
if c.Settings == nil || len(c.Settings) == 0 {
|
||||
if len(c.Settings) == 0 {
|
||||
c.Settings = json.RawMessage(`{}`)
|
||||
}
|
||||
row := s.Pool.QueryRow(ctx, `
|
||||
@@ -138,7 +138,7 @@ RETURNING id, name, kind, target, settings, active, created_at, updated_at`,
|
||||
}
|
||||
|
||||
func (s *Service) UpdateChannel(ctx context.Context, id int64, c Channel) (*Channel, error) {
|
||||
if c.Settings == nil || len(c.Settings) == 0 {
|
||||
if len(c.Settings) == 0 {
|
||||
c.Settings = json.RawMessage(`{}`)
|
||||
}
|
||||
row := s.Pool.QueryRow(ctx, `
|
||||
@@ -269,7 +269,7 @@ func (s *Service) sendWebhook(ctx context.Context, c Channel, kind string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return fmt.Errorf("webhook %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
||||
|
||||
@@ -55,7 +55,7 @@ func PackageVersions(ctx context.Context, force bool) map[string]string {
|
||||
defer cancel()
|
||||
|
||||
for _, pkg := range edgePackages {
|
||||
installed, candidate := aptCachePolicy(policyCtx, pkg)
|
||||
installed, candidate := aptCachePolicy(policyCtx, pkg) //nolint:contextcheck // detached by design — Cache-Read überlebt Request-Cancel
|
||||
out[pkg+"_installed"] = installed
|
||||
out[pkg+"_available"] = candidate
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -232,10 +231,9 @@ func (s *Service) Run(ctx context.Context, kind Kind, version string) (*Result,
|
||||
s.recordFailure(ctx, res, hostname, kind)
|
||||
return res, err
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
// Nicht fatal — fsync-failure kann passieren bei tmpfs in
|
||||
// Dev, aber der File ist da.
|
||||
}
|
||||
// fsync best-effort — Nicht fatal: fsync-failure kann bei tmpfs in
|
||||
// Dev passieren, der File ist trotzdem da.
|
||||
_ = f.Sync()
|
||||
if err := f.Close(); err != nil {
|
||||
s.recordFailure(ctx, res, hostname, kind)
|
||||
return res, err
|
||||
@@ -644,10 +642,6 @@ func (b *bytes) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// strFold ist ein utility nur zum Defensiv-Check, dass kind ein
|
||||
// erlaubter Wert ist (für die DB-Constraint).
|
||||
func strFold(s string) string { return strings.ToLower(strings.TrimSpace(s)) }
|
||||
|
||||
// SortByDate sortiert Entries newest-first. Wird nicht direkt benutzt
|
||||
// (DB-Query macht's), aber praktisch wenn der Caller eine eigene
|
||||
// Liste hat.
|
||||
|
||||
@@ -180,7 +180,7 @@ func uploadS3(ctx context.Context, s S3Settings, localPath string) (int64, error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -245,12 +245,12 @@ func uploadSFTP(ctx context.Context, s SFTPSettings, localPath string) (int64, e
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("ssh dial %s: %w", addr, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
defer func() { _ = conn.Close() }()
|
||||
cl, err := sftp.NewClient(conn)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("sftp client: %w", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
defer func() { _ = cl.Close() }()
|
||||
|
||||
// remote-dir anlegen (idempotent)
|
||||
_ = cl.MkdirAll(s.RemoteDir)
|
||||
@@ -259,7 +259,7 @@ func uploadSFTP(ctx context.Context, s SFTPSettings, localPath string) (int64, e
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer src.Close()
|
||||
defer func() { _ = src.Close() }()
|
||||
stat, err := src.Stat()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -269,10 +269,16 @@ func uploadSFTP(ctx context.Context, s SFTPSettings, localPath string) (int64, e
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create remote: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
_ = dst.Close()
|
||||
return 0, fmt.Errorf("copy: %w", err)
|
||||
}
|
||||
// SFTP-Close flusht gepufferte Writes — ein Fehler hier bedeutet
|
||||
// ein unvollständiges Remote-File und MUSS als Upload-Fehler
|
||||
// gemeldet werden (nicht als Erfolg durchgehen).
|
||||
if err := dst.Close(); err != nil {
|
||||
return 0, fmt.Errorf("close remote: %w", err)
|
||||
}
|
||||
return stat.Size(), nil
|
||||
}
|
||||
|
||||
@@ -284,9 +290,9 @@ func (s *Service) Test(ctx context.Context, t Target) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
defer func() { _ = os.Remove(tmp.Name()) }()
|
||||
_, _ = tmp.WriteString("edgeguard remote-target test " + time.Now().Format(time.RFC3339))
|
||||
tmp.Close()
|
||||
_ = tmp.Close()
|
||||
|
||||
r := s.uploadOne(ctx, t, tmp.Name())
|
||||
if !r.OK {
|
||||
@@ -331,9 +337,9 @@ func (s *Service) Test(ctx context.Context, t Target) error {
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second}); err == nil {
|
||||
if cl, err := sftp.NewClient(conn); err == nil {
|
||||
_ = cl.Remove(strings.TrimRight(settings.RemoteDir, "/") + "/" + filepath.Base(tmp.Name()))
|
||||
cl.Close()
|
||||
_ = cl.Close()
|
||||
}
|
||||
conn.Close()
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package clusterjoin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
@@ -177,7 +178,7 @@ func NormalizePrimaryURL(in string) (string, error) {
|
||||
|
||||
func issueCert(primary, token, csr string, insecure bool) (caCert, peerCert string, err error) {
|
||||
body, _ := json.Marshal(map[string]string{"token": token, "csr": csr})
|
||||
req, err := http.NewRequest(http.MethodPost,
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
|
||||
primary+"/api/v1/cluster/issue-cert", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -193,7 +194,7 @@ func issueCert(primary, token, csr string, insecure bool) (caCert, peerCert stri
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
@@ -287,7 +288,7 @@ func autoRegister(primary, tlsDir, commonName, version, nodeID, configHash, role
|
||||
}
|
||||
client := &http.Client{Transport: tr, Timeout: 30 * time.Second}
|
||||
|
||||
httpReq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
|
||||
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, u.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -296,7 +297,7 @@ func autoRegister(primary, tlsDir, commonName, version, nodeID, configHash, role
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
|
||||
@@ -97,7 +97,7 @@ func (r *AddressGroupsRepo) Create(ctx context.Context, g models.FirewallAddress
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO firewall_address_groups (name, description)
|
||||
@@ -125,7 +125,7 @@ func (r *AddressGroupsRepo) Update(ctx context.Context, id int64, g models.Firew
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE firewall_address_groups SET name = $1, description = $2, updated_at = NOW()
|
||||
|
||||
@@ -91,7 +91,7 @@ func (r *ServiceGroupsRepo) Create(ctx context.Context, g models.FirewallService
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO firewall_service_groups (name, description) VALUES ($1, $2)
|
||||
@@ -115,7 +115,7 @@ func (r *ServiceGroupsRepo) Update(ctx context.Context, id int64, g models.Firew
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
UPDATE firewall_service_groups SET name = $1, description = $2, updated_at = NOW()
|
||||
|
||||
@@ -102,7 +102,7 @@ func ReadTail(path string, f Filter) ([]Entry, error) {
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
// Ring-Buffer für die letzten f.Limit matching entries.
|
||||
buf := make([]Entry, 0, f.Limit)
|
||||
|
||||
@@ -64,7 +64,7 @@ func (t *Tailer) Start(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
defer func() { _ = w.Close() }()
|
||||
|
||||
// Watch das Verzeichnis statt der Datei — wenn ulogd das File
|
||||
// erstmals anlegt (oder rotate-rename), bekommen wir Create-Events.
|
||||
@@ -106,7 +106,7 @@ func (t *Tailer) bootstrap() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for sc.Scan() {
|
||||
@@ -134,7 +134,7 @@ func (t *Tailer) drainFile() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
|
||||
@@ -292,7 +292,7 @@ func parseWGConf(path string) (*parsedConf, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
var (
|
||||
out parsedConf
|
||||
|
||||
Reference in New Issue
Block a user