diff --git a/.github/workflows/deploy-staging-k8s.yml b/.github/workflows/deploy-staging-k8s.yml new file mode 100644 index 0000000..34fa15a --- /dev/null +++ b/.github/workflows/deploy-staging-k8s.yml @@ -0,0 +1,76 @@ +name: Ghost Ops - API & Worker Deployment + +on: + push: + branches: [staging] + +permissions: + contents: write + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare variables + id: vars + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "owner_lc=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # --- Budowanie API --- + - name: Build & Push API + uses: docker/build-push-action@v5 + with: + context: . + file: cmd/api/Dockerfile + push: true + tags: ghcr.io/${{ steps.vars.outputs.owner_lc }}/http-server-api:${{ steps.vars.outputs.sha_short }} + + # --- Budowanie WORKERA --- + - name: Build & Push Worker + uses: docker/build-push-action@v5 + with: + context: . + file: cmd/worker/Dockerfile + push: true + tags: ghcr.io/${{ steps.vars.outputs.owner_lc }}/http-server-worker:${{ steps.vars.outputs.sha_short }} + + # --- ENTERPRISE GITOPS: Aktualizacja OSOBNEGO repozytorium infrastruktury --- + - name: Update Infrastructure Repo + env: + INFRA_TOKEN: ${{ secrets.INFRA_REPO_TOKEN }} + SHA: ${{ steps.vars.outputs.sha_short }} + OWNER: ${{ steps.vars.outputs.owner_lc }} + run: | + # 1. Konfiguracja tożsamości Gita + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + + # 2. Klonowanie repozytorium INFRA (używamy PAT do autoryzacji) + # Zwróć uwagę na nazwę repo: k3s-gitops-infra + git clone https://$INFRA_TOKEN@github.com/${{ github.repository_owner }}/k3s-gitops-infra.git temp_infra + + cd temp_infra + + # 3. Aktualizacja tagów obrazów w nowej strukturze folderów + # Szukamy linii z obrazem i podmieniamy na nowy SHA + echo "Updating images to version: $SHA" + + sed -i "s|image: ghcr.io/.*/http-server-api:.*|image: ghcr.io/$OWNER/http-server-api:$SHA|g" overlays/staging/api/deployment.yaml + sed -i "s|image: ghcr.io/.*/http-server-worker:.*|image: ghcr.io/$OWNER/http-server-worker:$SHA|g" overlays/staging/worker/deployment.yaml + + # 4. Commit i Push zmian do repozytorium INFRA + git add . + git commit -m "Deploy: API & Worker update to $SHA (triggered by http-server commit)" || echo "No changes to commit" + git push origin main diff --git a/cmd/api/.dockerignore b/cmd/api/.dockerignore new file mode 100644 index 0000000..53428b1 --- /dev/null +++ b/cmd/api/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +docker-compose* +Dockerfile* +bin +obj +*.md \ No newline at end of file diff --git a/cmd/api/Dockerfile b/cmd/api/Dockerfile index 99a1f3e..3ef80db 100644 --- a/cmd/api/Dockerfile +++ b/cmd/api/Dockerfile @@ -1,20 +1,43 @@ -# Dockerfile.api +FROM golang:1.26-alpine AS builder -FROM golang:1.25-alpine AS builder +RUN apk add --no-cache ca-certificates git tzdata -WORKDIR /app +WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/api +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags="-w -s" -o /app/server ./cmd/api -FROM alpine:latest +FROM alpine:3.19.1 AS final -WORKDIR /app +LABEL org.opencontainers.image.source="https://github.com/ZeroDayZ7/http-server" +LABEL org.opencontainers.image.description="Hardened Go API" -COPY --from=builder /app/app . +RUN apk update && \ + apk add --no-cache ca-certificates tzdata && \ + rm -rf /var/cache/apk/* -CMD ["./app"] \ No newline at end of file +RUN addgroup -S appgroup && \ + adduser -S -u 10001 -g appgroup appuser + +WORKDIR /home/appuser + +COPY --from=builder --chown=appuser:appgroup /app/server . +COPY --from=builder --chown=appuser:appgroup /src/migrations ./migrations + +RUN chmod 500 ./server && \ + chmod 500 ./migrations && \ + chmod 400 ./migrations/* + +USER appuser + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +ENTRYPOINT ["./server"] diff --git a/cmd/api/main.go b/cmd/api/main.go index e604f94..7bea611 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -43,7 +43,7 @@ func main() { defer stop() // 3. Inicjalizacja infrastruktury - db, err := config.InitDB(ctx, cfg.Database, log) + db, err := config.InitDB(ctx, cfg.Database, log, true) if err != nil { log.Fatal("Database initialization failed", zap.Error(err)) } diff --git a/cmd/worker/.dockerignore b/cmd/worker/.dockerignore new file mode 100644 index 0000000..53428b1 --- /dev/null +++ b/cmd/worker/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +docker-compose* +Dockerfile* +bin +obj +*.md \ No newline at end of file diff --git a/cmd/worker/Dockerfile b/cmd/worker/Dockerfile index 49f3c63..6155c8f 100644 --- a/cmd/worker/Dockerfile +++ b/cmd/worker/Dockerfile @@ -1,22 +1,35 @@ -# Dockerfile.worker - -FROM golang:1.25-alpine AS builder - -WORKDIR /app +FROM golang:1.26-alpine AS builder +RUN apk add --no-cache ca-certificates git tzdata +WORKDIR /src COPY go.mod go.sum ./ RUN go mod download - COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o worker ./cmd/worker +# Dodano -trimpath dla bezpieczeństwa logów +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags="-w -s" -o /app/worker ./cmd/worker + +FROM alpine:3.19 AS final + +# Dodano LABEL dla porządku w GitHubie +LABEL org.opencontainers.image.source="https://github.com/ZeroDayZ7/http-server" +LABEL org.opencontainers.image.description="Hardened Go Worker" + +# Dodano apk update i czyszczenie cache +RUN apk update && \ + apk add --no-cache ca-certificates tzdata && \ + rm -rf /var/cache/apk/* + +RUN addgroup -S workergroup && \ + adduser -S -u 10002 -g workergroup workeruser -FROM alpine:latest +WORKDIR /home/workeruser -WORKDIR /app +COPY --from=builder --chown=workeruser:workergroup /app/worker . -COPY --from=builder /app/worker . +RUN chmod 500 ./worker -COPY --from=builder /app/migrations ./migrations +USER workeruser -CMD ["./worker"] \ No newline at end of file +ENTRYPOINT ["./worker"] diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 47493b8..f18154f 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -60,7 +60,7 @@ func main() { g, gCtx := errgroup.WithContext(ctx) // infra init - db, err := config.InitDB(ctx, cfg.Database, log) + db, err := config.InitDB(ctx, cfg.Database, log, false) if err != nil { log.Fatal("Database initialization failed", zap.Error(err)) } diff --git a/config/app.go b/config/app.go index fafd41e..6a5af3b 100644 --- a/config/app.go +++ b/config/app.go @@ -42,7 +42,7 @@ func NewFiberApp(cfg *env.Config, log logger.Logger) *fiber.App { app.Use(cors.New(CorsConfig(cfg))) app.Use(helmet.New(HelmetConfig())) - // app.Use(NewLimiter(cfg, "global")) + app.Use(GetLimiter(cfg, LimitApp)) app.Use(compress.New(CompressConfig())) return app diff --git a/config/db.go b/config/db.go index 74f92f5..86dff74 100644 --- a/config/db.go +++ b/config/db.go @@ -12,7 +12,7 @@ import ( "go.uber.org/zap" ) -func InitDB(ctx context.Context, cfg env.DBConfig, log logger.Logger) (*sql.DB, error) { +func InitDB(ctx context.Context, cfg env.DBConfig, log logger.Logger, runMigrations bool) (*sql.DB, error) { dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?parseTime=true", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName, @@ -34,10 +34,14 @@ func InitDB(ctx context.Context, cfg env.DBConfig, log logger.Logger) (*sql.DB, return nil, fmt.Errorf("db ping failed: %w", err) } - if err := RunMigrations(db, log); err != nil { - return nil, fmt.Errorf("migrations failed: %w", err) + if runMigrations { + if err := RunMigrations(db, log); err != nil { + return nil, fmt.Errorf("migrations failed: %w", err) + } + log.Info("MySQL connected and migrations applied", zap.String("database", cfg.DBName)) + } else { + log.Info("MySQL connected (migrations skipped)", zap.String("database", cfg.DBName)) } - log.Info("MySQL connected and migrations applied", zap.String("database", cfg.DBName)) return db, nil } diff --git a/config/env/defaults.go b/config/env/defaults.go index 5cc7b70..08ee571 100644 --- a/config/env/defaults.go +++ b/config/env/defaults.go @@ -10,7 +10,7 @@ func setDefaults() { // Server viper.SetDefault("APP_NAME", "http-server") viper.SetDefault("PORT", "8080") - viper.SetDefault("WORKERPORT", "8081") + viper.SetDefault("WORKER_PORT", "8081") viper.SetDefault("BODY_LIMIT_MB", 2) viper.SetDefault("APP_VERSION", "0.1.0") viper.SetDefault("ENV", "development") @@ -27,7 +27,7 @@ func setDefaults() { viper.SetDefault("DB_PASSWORD", "password") viper.SetDefault("DB_HOST", "localhost") viper.SetDefault("DB_PORT", "3306") - viper.SetDefault("DB_NAME", "appdb") + viper.SetDefault("DB_NAME", "portfolio_db") viper.SetDefault("DB_MAX_OPEN_CONNS", 50) viper.SetDefault("DB_MAX_IDLE_CONNS", 10) viper.SetDefault("DB_CONN_MAX_LIFETIME", 30*time.Minute) diff --git a/config/env/loader.go b/config/env/loader.go index 857fb84..8ac2f24 100644 --- a/config/env/loader.go +++ b/config/env/loader.go @@ -18,6 +18,10 @@ func LoadConfig(cfg *Config) error { viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.BindEnv("WORKER_PORT") + viper.BindEnv("DB_HOST") + viper.BindEnv("ENV") + // AUTO detect pliku .env if fileExists(".env") { viper.SetConfigFile(".env") diff --git a/config/limiter.go b/config/limiter.go index e0ded06..e3652c5 100644 --- a/config/limiter.go +++ b/config/limiter.go @@ -1,6 +1,7 @@ package config import ( + "sync" "time" "github.com/gofiber/fiber/v2" @@ -11,48 +12,95 @@ import ( "github.com/zerodayz7/http-server/internal/errors" ) -func NewLimiter(cfg *env.Config, group string) fiber.Handler { - presets := map[string]struct { +type LimitGroup string + +const ( + LimitApp LimitGroup = "app" + LimitGlobal LimitGroup = "global" + LimitHealth LimitGroup = "health" + LimitVisits LimitGroup = "visits" +) + +var ( + storage fiber.Storage + storageOnce sync.Once + + limiters = make(map[LimitGroup]fiber.Handler) + limitersLock sync.RWMutex +) + +func getStorage(cfg *env.Config) fiber.Storage { + if cfg.Redis.Host == "" || cfg.Redis.Host == "memory" { + return nil + } + + storageOnce.Do(func() { + storage = fiberRedis.New(fiberRedis.Config{ + Host: cfg.Redis.Host, + Port: cfg.Redis.Port, + Password: cfg.Redis.Password, + Database: cfg.Redis.DB, + }) + }) + + return storage +} + +func GetLimiter(cfg *env.Config, group LimitGroup) fiber.Handler { + + limitersLock.RLock() + if l, exists := limiters[group]; exists { + limitersLock.RUnlock() + return l + } + limitersLock.RUnlock() + + limitersLock.Lock() + defer limitersLock.Unlock() + + if l, exists := limiters[group]; exists { + return l + } + + l := createLimiter(cfg, group) + limiters[group] = l + + return l +} + +func createLimiter(cfg *env.Config, group LimitGroup) fiber.Handler { + presets := map[LimitGroup]struct { Max int Window time.Duration }{ - "global": {Max: cfg.RateLimit.Max, Window: cfg.RateLimit.Window}, - "health": {Max: 50, Window: 1 * time.Minute}, - "visits": {Max: 30, Window: 30 * time.Minute}, + LimitApp: {Max: cfg.RateLimit.Max, Window: cfg.RateLimit.Window}, + LimitGlobal: {Max: 100, Window: 1 * time.Minute}, + LimitHealth: {Max: 50, Window: 1 * time.Minute}, + LimitVisits: {Max: 30, Window: 30 * time.Minute}, } limit, ok := presets[group] if !ok { - limit = presets["global"] + limit = presets[LimitGlobal] } limiterConfig := limiter.Config{ Max: limit.Max, Expiration: limit.Window, + Storage: getStorage(cfg), KeyGenerator: func(c *fiber.Ctx) string { - testKey := c.Get("X-Test-IP") - if testKey != "" { + if testKey := c.Get("X-Test-IP"); testKey != "" { return testKey } return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return errors.ErrTooManyRequests. - WithDetail("group", group). + WithDetail("group", string(group)). WithDetail("ip", c.IP()). WithDetail("path", c.Path()) }, } - // --- LOGIKA STORAGE --- - if cfg.Redis.Host != "" && cfg.Redis.Host != "memory" { - limiterConfig.Storage = fiberRedis.New(fiberRedis.Config{ - Host: cfg.Redis.Host, - Port: cfg.Redis.Port, - Password: cfg.Redis.Password, - Database: cfg.Redis.DB, - }) - } - return limiter.New(limiterConfig) } diff --git a/docs/0_index.md b/docs/0_index.md new file mode 100644 index 0000000..c5b4e90 --- /dev/null +++ b/docs/0_index.md @@ -0,0 +1,10 @@ +## Spis treści +[1_Introduction](1_Introduction.md). + +[2_migrations](2_migrations.md). + +[command](command.md). + +[internal](internal.md). + + diff --git a/docs/1 Uruchomienie.md b/docs/1 Uruchomienie.md deleted file mode 100644 index c8c4743..0000000 --- a/docs/1 Uruchomienie.md +++ /dev/null @@ -1,32 +0,0 @@ -### Run - -```bash -go run ./cmd -``` - -### Build - -```bash -go build -o bin/server ./cmd -``` -```bash -GOOS=freebsd GOARCH=amd64 go build -o ./bin/server ./cmd - -``` - ---- - -| Komenda | Opis | Przykład | -| --------------------- | ------------------------------------------------------------------ | ------------------------------- | -| `go version` | Sprawdza zainstalowaną wersję Go. | `go version` | -| `go env` | Wyświetla zmienne środowiskowe Go (np. `GOPATH`, `GOROOT`). | `go env` | -| `go mod init ` | Inicjuje nowy moduł Go w bieżącym folderze (tworzy plik `go.mod`). | `go mod init myapp` | -| `go mod tidy` | Dodaje brakujące zależności i usuwa nieużywane. | `go mod tidy` | -| `go get ` | Pobiera i instaluje zewnętrzny pakiet. | `go get github.com/gorilla/mux` | -| `go build` | Kompiluje projekt do pliku wykonywalnego (binarki). | `go build` | -| `go run ` | Uruchamia kod bez wcześniejszej kompilacji. | `go run main.go` | -| `go test` | Uruchamia testy w katalogu. | `go test` | -| `go install` | Kompiluje i instaluje aplikację w `$GOPATH/bin`. | `go install` | -| `go clean` | Usuwa pliki tymczasowe po kompilacji. | `go clean` | - ---- diff --git a/docs/1_Introduction.md b/docs/1_Introduction.md new file mode 100644 index 0000000..f12ff7c --- /dev/null +++ b/docs/1_Introduction.md @@ -0,0 +1,15 @@ +### Run + +```bash +go run ./cmd +``` + +### Build + +```bash +go build -o bin/server ./cmd +``` +```bash +GOOS=freebsd GOARCH=amd64 go build -o ./bin/server ./cmd + +``` \ No newline at end of file diff --git a/docs/2 migrations.md b/docs/2_migrations.md similarity index 100% rename from docs/2 migrations.md rename to docs/2_migrations.md diff --git a/docs/command.md b/docs/command.md index aa1be89..aa2aa26 100644 --- a/docs/command.md +++ b/docs/command.md @@ -6,3 +6,15 @@ curl -v http://localhost:9001/health # Sprawdź swoje API curl -v http://localhost:9000/health + +govulncheck ./... +go mod edit -go 1.26.2 +go mod tidy + +go install golang.org/x/vuln/cmd/govulncheck@latest + +# 1. Upewnij się, że jesteś w głównym folderze projektu +cd C:\Users\Neo\Desktop\WWW\go\http-server + +# 2. Uruchom mockery z precyzyjnymi ustawieniami +mockery --dir=internal/service --all --output=internal/service/mocks --outpkg=mocks diff --git a/go.mod b/go.mod index de69e44..b77440a 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/zerodayz7/http-server -go 1.25.0 +go 1.26.2 require ( github.com/go-playground/validator/v10 v10.30.1 github.com/go-sql-driver/mysql v1.9.3 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/gofiber/fiber/v2 v2.52.12 + github.com/gofiber/storage/redis/v3 v3.4.3 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/uuid v1.6.0 github.com/google/wire v0.7.0 @@ -42,7 +43,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gofiber/storage/redis/v3 v3.4.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 65a9860..2188269 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,9 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -13,6 +14,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -23,6 +26,12 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -32,14 +41,14 @@ github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -53,6 +62,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -69,8 +80,8 @@ github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/ github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/storage/redis/v3 v3.4.3 h1:PvazbTpDAvmDHpMk4fCvCoTXm+neLXQL1rWuHTXlNz8= github.com/gofiber/storage/redis/v3 v3.4.3/go.mod h1:n/wFsaS4cwfRQERwhkZhMmJrNFAf514MaWL7ky33sTk= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gofiber/storage/testhelpers/redis v0.1.0 h1:lDUwtanDf3f5YwlDwhbqnqCtj9Y/xc8ctxRE6HpQcws= +github.com/gofiber/storage/testhelpers/redis v0.1.0/go.mod h1:Y1UccxbGVL04+TF5RuyCsksX+76hu6nJIWjPukBBgJ4= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -91,30 +102,46 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/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.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= @@ -124,6 +151,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -138,6 +167,10 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= +github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -156,21 +189,30 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/redis v0.40.0 h1:OG4qwcxp2O0re7V7M9lY9w0v6wWgWf7j7rtkpAnGMd0= +github.com/testcontainers/testcontainers-go/modules/redis v0.40.0/go.mod h1:Bc+EDhKMo5zI5V5zdBkHiMVzeAXbtI4n5isS/nzf6zw= github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +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/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= diff --git a/internal/router/fallback.go b/internal/router/fallback.go index 2c88cb9..ab1642a 100644 --- a/internal/router/fallback.go +++ b/internal/router/fallback.go @@ -6,11 +6,13 @@ import ( "go.uber.org/zap" ) -func SetupFallbackHandlers(app *fiber.App, log logger.Logger) { +func SetupFavicon(app *fiber.App) { app.Get("/favicon.ico", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) }) +} +func SetupNotFoundHandler(app *fiber.App, log logger.Logger) { app.Use(func(c *fiber.Ctx) error { log.Warn("404 - not found", zap.String("path", c.Path()), diff --git a/internal/router/health_routes.go b/internal/router/health_routes.go index 0794df4..b94a0de 100644 --- a/internal/router/health_routes.go +++ b/internal/router/health_routes.go @@ -11,7 +11,7 @@ import ( func SetupHealthRoutes(app *fiber.App, cfg *env.Config) { health := app.Group("/health") - health.Use(config.NewLimiter(cfg, "health")) + health.Use(config.GetLimiter(cfg, config.LimitHealth)) health.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ diff --git a/internal/router/interaction_router.go b/internal/router/interaction_router.go index 9db5f57..de9f217 100644 --- a/internal/router/interaction_router.go +++ b/internal/router/interaction_router.go @@ -11,7 +11,7 @@ import ( func SetupStatsRoutes(app *fiber.App, h *handler.InteractionHandler, cfg *env.Config) { stats := app.Group("/stats") - stats.Use(config.NewLimiter(cfg, "visits")) + stats.Use(config.GetLimiter(cfg, config.LimitVisits)) stats.Get("/interactions", middleware.ValidateQuery[validator.FingerprintRequest](), diff --git a/internal/router/routes.go b/internal/router/routes.go index 352d32c..a79d055 100644 --- a/internal/router/routes.go +++ b/internal/router/routes.go @@ -2,6 +2,7 @@ package router import ( "github.com/gofiber/fiber/v2" + "github.com/zerodayz7/http-server/config" "github.com/zerodayz7/http-server/config/env" "github.com/zerodayz7/http-server/internal/handler" "github.com/zerodayz7/http-server/internal/shared/logger" @@ -13,7 +14,13 @@ func SetupRoutes( cfg *env.Config, log logger.Logger, ) { + SetupFavicon(app) + SetupHealthRoutes(app, cfg) SetupStatsRoutes(app, interactionHandler, cfg) - SetupFallbackHandlers(app, log) + + api := app.Group("/") + api.Use(config.GetLimiter(cfg, config.LimitGlobal)) + + SetupNotFoundHandler(app, log) } diff --git a/internal/service/interaction_service_test.go b/internal/service/interaction_service_test.go new file mode 100644 index 0000000..35f6692 --- /dev/null +++ b/internal/service/interaction_service_test.go @@ -0,0 +1,48 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/zerodayz7/http-server/internal/service" + "github.com/zerodayz7/http-server/internal/service/mocks" +) + +func TestProcessInitialVisit_ShouldNotPublishIfAlreadyRecorded(t *testing.T) { + // 1. GEST (Setup) - Tworzymy mocki dla zależności + mockRepo := new(mocks.InteractionRepository) + mockCache := new(mocks.InteractionCache) + mockPublisher := new(mocks.EventPublisher) + mockIdentity := new(mocks.IdentityService) + // Loggera możemy zostawić jako nil lub dodać prosty mock, + // jeśli serwis go nie używa intensywnie do logiki. + + s := service.NewInteractionService(mockRepo, mockCache, mockPublisher, mockIdentity, nil) + + ctx := context.Background() + fp := "fake-fingerprint" + + // 2. PROGRAMOWANIE (Expectations) + // Mówimy: TryRecordVisit ma zwrócić FALSE (użytkownik już tu był) + mockCache.On("TryRecordVisit", ctx, fp, service.Cooldown).Return(false, nil) + + // Mówimy: Pobieranie statystyk ma zwrócić jakieś dane + mockCache.On("GetGlobalCount", ctx, mock.Anything).Return(int64(100), true) + mockCache.On("GetUserChoice", ctx, fp).Return("", false, nil) + + // 3. DZIAŁANIE (Execution) + res, err := s.ProcessInitialVisit(ctx, fp) + + // 4. SPRAWDZENIE (Assertions) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, int64(100), res.Visits) + + // KLUCZOWE: Sprawdzamy, czy PublishInteraction NIGDY nie został wywołany + mockPublisher.AssertNotCalled(t, "PublishInteraction", mock.Anything, mock.Anything, mock.Anything) + + // Sprawdzamy czy wszystkie zaprogramowane metody zostały zawołane + mockCache.AssertExpectations(t) +} diff --git a/internal/service/mocks/EventPublisher.go b/internal/service/mocks/EventPublisher.go new file mode 100644 index 0000000..884fa21 --- /dev/null +++ b/internal/service/mocks/EventPublisher.go @@ -0,0 +1,46 @@ +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// EventPublisher is an autogenerated mock type for the EventPublisher type +type EventPublisher struct { + mock.Mock +} + +// PublishInteraction provides a mock function with given fields: ctx, typ, fp +func (_m *EventPublisher) PublishInteraction(ctx context.Context, typ string, fp string) error { + ret := _m.Called(ctx, typ, fp) + + if len(ret) == 0 { + panic("no return value specified for PublishInteraction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, typ, fp) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewEventPublisher creates a new instance of EventPublisher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEventPublisher(t interface { + mock.TestingT + Cleanup(func()) +}) *EventPublisher { + mock := &EventPublisher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mocks/IdentityService.go b/internal/service/mocks/IdentityService.go new file mode 100644 index 0000000..15a6132 --- /dev/null +++ b/internal/service/mocks/IdentityService.go @@ -0,0 +1,42 @@ +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// IdentityService is an autogenerated mock type for the IdentityService type +type IdentityService struct { + mock.Mock +} + +// GenerateFingerprint provides a mock function with given fields: ip, ua, lang +func (_m *IdentityService) GenerateFingerprint(ip string, ua string, lang string) string { + ret := _m.Called(ip, ua, lang) + + if len(ret) == 0 { + panic("no return value specified for GenerateFingerprint") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, string, string) string); ok { + r0 = rf(ip, ua, lang) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// NewIdentityService creates a new instance of IdentityService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIdentityService(t interface { + mock.TestingT + Cleanup(func()) +}) *IdentityService { + mock := &IdentityService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mocks/InteractionCache.go b/internal/service/mocks/InteractionCache.go new file mode 100644 index 0000000..60915a3 --- /dev/null +++ b/internal/service/mocks/InteractionCache.go @@ -0,0 +1,167 @@ +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// InteractionCache is an autogenerated mock type for the InteractionCache type +type InteractionCache struct { + mock.Mock +} + +// GetGlobalCount provides a mock function with given fields: ctx, typ +func (_m *InteractionCache) GetGlobalCount(ctx context.Context, typ string) (int64, bool) { + ret := _m.Called(ctx, typ) + + if len(ret) == 0 { + panic("no return value specified for GetGlobalCount") + } + + var r0 int64 + var r1 bool + if rf, ok := ret.Get(0).(func(context.Context, string) (int64, bool)); ok { + return rf(ctx, typ) + } + if rf, ok := ret.Get(0).(func(context.Context, string) int64); ok { + r0 = rf(ctx, typ) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) bool); ok { + r1 = rf(ctx, typ) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// GetUserChoice provides a mock function with given fields: ctx, fp +func (_m *InteractionCache) GetUserChoice(ctx context.Context, fp string) (string, bool, error) { + ret := _m.Called(ctx, fp) + + if len(ret) == 0 { + panic("no return value specified for GetUserChoice") + } + + var r0 string + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, bool, error)); ok { + return rf(ctx, fp) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, fp) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) bool); ok { + r1 = rf(ctx, fp) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, fp) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// SetGlobalCount provides a mock function with given fields: ctx, typ, count, ttl +func (_m *InteractionCache) SetGlobalCount(ctx context.Context, typ string, count int64, ttl time.Duration) error { + ret := _m.Called(ctx, typ, count, ttl) + + if len(ret) == 0 { + panic("no return value specified for SetGlobalCount") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64, time.Duration) error); ok { + r0 = rf(ctx, typ, count, ttl) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TryRecordInteraction provides a mock function with given fields: ctx, fp, typ, cooldown +func (_m *InteractionCache) TryRecordInteraction(ctx context.Context, fp string, typ string, cooldown time.Duration) (bool, error) { + ret := _m.Called(ctx, fp, typ, cooldown) + + if len(ret) == 0 { + panic("no return value specified for TryRecordInteraction") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, time.Duration) (bool, error)); ok { + return rf(ctx, fp, typ, cooldown) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, time.Duration) bool); ok { + r0 = rf(ctx, fp, typ, cooldown) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, time.Duration) error); ok { + r1 = rf(ctx, fp, typ, cooldown) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TryRecordVisit provides a mock function with given fields: ctx, fp, cooldown +func (_m *InteractionCache) TryRecordVisit(ctx context.Context, fp string, cooldown time.Duration) (bool, error) { + ret := _m.Called(ctx, fp, cooldown) + + if len(ret) == 0 { + panic("no return value specified for TryRecordVisit") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, time.Duration) (bool, error)); ok { + return rf(ctx, fp, cooldown) + } + if rf, ok := ret.Get(0).(func(context.Context, string, time.Duration) bool); ok { + r0 = rf(ctx, fp, cooldown) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, time.Duration) error); ok { + r1 = rf(ctx, fp, cooldown) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewInteractionCache creates a new instance of InteractionCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInteractionCache(t interface { + mock.TestingT + Cleanup(func()) +}) *InteractionCache { + mock := &InteractionCache{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mocks/InteractionRepository.go b/internal/service/mocks/InteractionRepository.go new file mode 100644 index 0000000..c27cbb2 --- /dev/null +++ b/internal/service/mocks/InteractionRepository.go @@ -0,0 +1,93 @@ +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + service "github.com/zerodayz7/http-server/internal/service" +) + +// InteractionRepository is an autogenerated mock type for the InteractionRepository type +type InteractionRepository struct { + mock.Mock +} + +// GetStats provides a mock function with given fields: ctx +func (_m *InteractionRepository) GetStats(ctx context.Context) (service.InteractionStatsDTO, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetStats") + } + + var r0 service.InteractionStatsDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (service.InteractionStatsDTO, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) service.InteractionStatsDTO); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(service.InteractionStatsDTO) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Increment provides a mock function with given fields: ctx, typ +func (_m *InteractionRepository) Increment(ctx context.Context, typ string) error { + ret := _m.Called(ctx, typ) + + if len(ret) == 0 { + panic("no return value specified for Increment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, typ) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// IncrementBy provides a mock function with given fields: ctx, typ, amount +func (_m *InteractionRepository) IncrementBy(ctx context.Context, typ string, amount int64) error { + ret := _m.Called(ctx, typ, amount) + + if len(ret) == 0 { + panic("no return value specified for IncrementBy") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok { + r0 = rf(ctx, typ, amount) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewInteractionRepository creates a new instance of InteractionRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInteractionRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *InteractionRepository { + mock := &InteractionRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mocks/InteractionServiceInterface.go b/internal/service/mocks/InteractionServiceInterface.go new file mode 100644 index 0000000..62ec08e --- /dev/null +++ b/internal/service/mocks/InteractionServiceInterface.go @@ -0,0 +1,137 @@ +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + service "github.com/zerodayz7/http-server/internal/service" +) + +// InteractionServiceInterface is an autogenerated mock type for the InteractionServiceInterface type +type InteractionServiceInterface struct { + mock.Mock +} + +// GenerateFingerprint provides a mock function with given fields: ip, ua, lang +func (_m *InteractionServiceInterface) GenerateFingerprint(ip string, ua string, lang string) string { + ret := _m.Called(ip, ua, lang) + + if len(ret) == 0 { + panic("no return value specified for GenerateFingerprint") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string, string, string) string); ok { + r0 = rf(ip, ua, lang) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// GetStats provides a mock function with given fields: ctx, fp +func (_m *InteractionServiceInterface) GetStats(ctx context.Context, fp string) (*service.StatsResponse, error) { + ret := _m.Called(ctx, fp) + + if len(ret) == 0 { + panic("no return value specified for GetStats") + } + + var r0 *service.StatsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*service.StatsResponse, error)); ok { + return rf(ctx, fp) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *service.StatsResponse); ok { + r0 = rf(ctx, fp) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*service.StatsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, fp) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HandleInteraction provides a mock function with given fields: ctx, fp, typ +func (_m *InteractionServiceInterface) HandleInteraction(ctx context.Context, fp string, typ string) (*service.StatsResponse, error) { + ret := _m.Called(ctx, fp, typ) + + if len(ret) == 0 { + panic("no return value specified for HandleInteraction") + } + + var r0 *service.StatsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*service.StatsResponse, error)); ok { + return rf(ctx, fp, typ) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *service.StatsResponse); ok { + r0 = rf(ctx, fp, typ) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*service.StatsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, fp, typ) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProcessInitialVisit provides a mock function with given fields: ctx, fp +func (_m *InteractionServiceInterface) ProcessInitialVisit(ctx context.Context, fp string) (*service.StatsResponse, error) { + ret := _m.Called(ctx, fp) + + if len(ret) == 0 { + panic("no return value specified for ProcessInitialVisit") + } + + var r0 *service.StatsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*service.StatsResponse, error)); ok { + return rf(ctx, fp) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *service.StatsResponse); ok { + r0 = rf(ctx, fp) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*service.StatsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, fp) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewInteractionServiceInterface creates a new instance of InteractionServiceInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInteractionServiceInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *InteractionServiceInterface { + mock := &InteractionServiceInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/shared/logger/factory.go b/internal/shared/logger/factory.go index e4c8de3..a68c01b 100644 --- a/internal/shared/logger/factory.go +++ b/internal/shared/logger/factory.go @@ -14,6 +14,8 @@ func NewLogger(env Env) Logger { switch env { case EnvProduction: core = createProductionCore() + case EnvStaging: + core = createStagingCore() case EnvTest: core = createTestCore() default: // Development @@ -31,16 +33,17 @@ func NewLogger(env Env) Logger { // --- POMOCNICZE FUNKCJE KREUJĄCE --- func createProductionCore() zapcore.Core { - fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) - - logFile := zapcore.AddSync(&lumberjack.Logger{ - Filename: "logs/app.log", - MaxSize: 10, - MaxBackups: 5, - Compress: true, - }) + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoder := zapcore.NewJSONEncoder(encoderConfig) + return zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zap.InfoLevel) +} - return zapcore.NewCore(fileEncoder, logFile, zap.InfoLevel) +func createStagingCore() zapcore.Core { + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoder := zapcore.NewJSONEncoder(encoderConfig) + return zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zap.DebugLevel) } func createDevelopmentCore() zapcore.Core { diff --git a/internal/shared/logger/types.go b/internal/shared/logger/types.go index e4ce2b5..a5bb055 100644 --- a/internal/shared/logger/types.go +++ b/internal/shared/logger/types.go @@ -4,6 +4,7 @@ type Env string const ( EnvProduction Env = "production" + EnvStaging Env = "staging" EnvDevelopment Env = "development" EnvTest Env = "test" ) diff --git a/internal/worker/flush.go b/internal/worker/flush.go index 9e7df81..f1e5439 100644 --- a/internal/worker/flush.go +++ b/internal/worker/flush.go @@ -16,7 +16,6 @@ func (w *InteractionWorker) safeFlush(ctx context.Context, batch *eventBatch) { return } - batch.clear() w.flushInProgress.Store(true) defer w.flushInProgress.Store(false) @@ -58,6 +57,7 @@ func (w *InteractionWorker) safeFlush(ctx context.Context, batch *eventBatch) { return } + batch.clear() w.flushDuration.Record(flushCtx, time.Since(startTime).Seconds()) w.eventCounter.Add(flushCtx, int64(len(ids))) w.markIdempotentBatch(flushCtx, ids)