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,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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user