diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7f31767..bf7867d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,9 @@ jobs: with: go-version: '1.26' + - name: Setup Docker + uses: docker/setup-buildx-action@v3 + - name: Tidy run: go mod tidy diff --git a/Dockerfile b/Dockerfile index bbaa46f..6838666 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,34 @@ -# Start from golang base image -FROM golang:1.26-alpine as builder -# Enable go modules +# Build stage +FROM golang:1.26-alpine AS builder + ENV GO111MODULE=on -# Enable release mode ENV GIN_MODE=release -LABEL maintainer="Alberto Adami " -LABEL org.opencontainers.image.source="https://github.com/albertoadami/nestled" -LABEL org.opencontainers.image.description="Nestled API" - -# Install bash. (alpine image does not have bash in it) -RUN apk update && apk add git && apk add bash +RUN apk add --no-cache git -# Set current working directory WORKDIR /app -# Note here: To avoid downloading dependencies every time we -# build image. Here, we are caching all the dependencies by -# first copying go.mod and go.sum files and downloading them, -# to be used every time we build the image if the dependencies -# are not changed. - -# Copy go mod and sum files -COPY go.mod ./ -COPY go.sum ./ - -# Download all dependencies. +COPY go.mod go.sum ./ RUN go mod download -# Now, copy the source code COPY . . -# Note here: CGO_ENABLED is disabled for cross system compilation -# It is also a common best practise. - -# Build the application. RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main ./cmd/main/main.go +# Final stage +FROM alpine:3.19 + +LABEL maintainer="Alberto Adami " +LABEL org.opencontainers.image.source="https://github.com/albertoadami/nestled" +LABEL org.opencontainers.image.description="Nestled API" + +RUN apk add --no-cache ca-certificates curl + +WORKDIR /app + +COPY --from=builder /app/main . +COPY --from=builder /app/config.yml . + EXPOSE 8080 -# Run executable + CMD ["./main"] \ No newline at end of file diff --git a/bin/nestled b/bin/nestled index d875e83..d8b0c9b 100755 Binary files a/bin/nestled and b/bin/nestled differ diff --git a/cmd/main/main.go b/cmd/main/main.go index a31d293..ee9b1a3 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -1,20 +1,46 @@ package main import ( + "github.com/albertoadami/nestled/internal/config" + "github.com/albertoadami/nestled/internal/database" "github.com/albertoadami/nestled/internal/handlers" + "github.com/albertoadami/nestled/internal/repositories" "github.com/albertoadami/nestled/internal/routes" + "github.com/albertoadami/nestled/internal/services" "github.com/gin-gonic/gin" + "go.uber.org/zap" ) func main() { + logger, _ := zap.NewProduction() + defer logger.Sync() + + logger.Info("Starting Nestled API server...") r := gin.Default() + configuration, err := config.LoadConfig() + if err != nil { + logger.Fatal("Failed to load configuration", zap.Error(err)) + } + + database, err := database.Connect(&configuration.Database) + if err != nil { + logger.Fatal("Failed to connect to database", zap.Error(err)) + } + defer database.Close() + + // repositories + userRepository := repositories.NewUserRepository(database) + + // services + userService := services.NewUserService(userRepository) + // Initialize handlers - healthHandler := handlers.NewHealthHandler() + userHandler := handlers.NewUserHandler(userService, logger) + healthHandler := handlers.NewHealthHandler(database) - // Setup routes - routes.SetupRoutes(r, healthHandler) + routes.SetupRoutes(r, userHandler, healthHandler) // Start the server if err := r.Run(); err != nil { diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..82e2b72 --- /dev/null +++ b/config.yml @@ -0,0 +1,6 @@ +database: + host: localhost + port: 5432 + user: postgres + password: password + name: nestled_db \ No newline at end of file diff --git a/go.mod b/go.mod index 9607112..e04646b 100644 --- a/go.mod +++ b/go.mod @@ -8,38 +8,102 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-migrate/migrate v3.5.4+incompatible // indirect github.com/golang-migrate/migrate/v4 v4.19.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.11.2 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/testcontainers/testcontainers-go v0.40.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/mock v0.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.47.0 // indirect golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index b53deca..e72f881 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,63 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+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.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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= @@ -26,17 +66,37 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= 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/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -49,53 +109,134 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +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-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-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +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.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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.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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.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-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/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/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/config/configuration.go b/internal/config/configuration.go new file mode 100644 index 0000000..da01ff5 --- /dev/null +++ b/internal/config/configuration.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + + "github.com/spf13/viper" +) + +type DatabaseConfig struct { + Host string + Port int + User string + Password string + Name string +} + +type Config struct { + Database DatabaseConfig +} + +func LoadConfig() (*Config, error) { + viper.SetConfigName("config") + viper.SetConfigType("yml") + viper.AddConfigPath(".") // local/dev + viper.AddConfigPath("/app") // Docker WORKDIR + viper.AddConfigPath("../..") // project root (for tests) + viper.AutomaticEnv() + + viper.BindEnv("database.host", "DB_HOST") + viper.BindEnv("database.port", "DB_PORT") + viper.BindEnv("database.user", "DB_USER") + viper.BindEnv("database.password", "DB_PASSWORD") + viper.BindEnv("database.name", "DB_NAME") + + if err := viper.ReadInConfig(); err != nil { + return nil, fmt.Errorf("fatal error config file: %w", err) + } + + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("unable to decode into struct: %w", err) + } + + return &cfg, nil +} diff --git a/internal/config/configuration_test.go b/internal/config/configuration_test.go new file mode 100644 index 0000000..ff53aba --- /dev/null +++ b/internal/config/configuration_test.go @@ -0,0 +1,25 @@ +package config + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestLoadConfigSuccessfully(t *testing.T) { + viper.AddConfigPath("../..") // add project root as config path + + configuration, error := LoadConfig() + + assert.NoError(t, error) + + dbConfig := configuration.Database + + assert.Equal(t, dbConfig.Host, "localhost") + assert.Equal(t, dbConfig.Port, 5432) + assert.Equal(t, dbConfig.User, "postgres") + assert.Equal(t, dbConfig.Password, "password") + assert.Equal(t, dbConfig.Name, "nestled_db") + +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..a1627b0 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,14 @@ +package crypto + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword verifies a plain text password against a hash +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..80a2adb --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,16 @@ +package database + +import ( + "fmt" + + "github.com/albertoadami/nestled/internal/config" + "github.com/jmoiron/sqlx" +) + +func Connect(config *config.DatabaseConfig) (*sqlx.DB, error) { + dsn := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + config.Host, config.Port, config.User, config.Password, config.Name, + ) + return sqlx.Connect("postgres", dsn) +} diff --git a/internal/dto/create_user_request.go b/internal/dto/create_user_request.go new file mode 100644 index 0000000..976e4e9 --- /dev/null +++ b/internal/dto/create_user_request.go @@ -0,0 +1,9 @@ +package dto + +type CreateUserRequest struct { + Username string `json:"username" binding:"required,min=3,max=50"` + FirstName string `json:"first_name" binding:"required,min=2,max=100"` + LastName string `json:"last_name" binding:"required,min=2,max=100"` + Email string `json:"email" binding:"required,email,max=255"` + Password string `json:"password" binding:"required,min=8,max=72"` +} diff --git a/internal/dto/errors.go b/internal/dto/errors.go new file mode 100644 index 0000000..e8d534f --- /dev/null +++ b/internal/dto/errors.go @@ -0,0 +1,18 @@ +package dto + +type ErrorResponse struct { + Message string `json:"message"` + Details string `json:"details,omitempty"` +} + +func NewErrorResponse(message string, details string) *ErrorResponse { + return &ErrorResponse{ + Message: message, + Details: details, + } +} + +const ( + ErrUsernameAlreadyExists = "username already exists" + ErrEmailAlreadyExists = "email already exists" +) diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..456138c --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import "errors" + +var ( + ErrUsernameAlreadyExists = errors.New("username already exists") + ErrEmailAlreadyExists = errors.New("email already exists") + ErrPasswordTooWeak = errors.New("password is too weak") +) diff --git a/internal/handlers/health_handler.go b/internal/handlers/health_handler.go index caf8a86..20db97f 100644 --- a/internal/handlers/health_handler.go +++ b/internal/handlers/health_handler.go @@ -1,13 +1,24 @@ package handlers -import "github.com/gin-gonic/gin" +import ( + "net/http" -type HealthHandler struct{} + "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" +) -func NewHealthHandler() *HealthHandler { - return &HealthHandler{} +type HealthHandler struct{ db *sqlx.DB } + +func NewHealthHandler(db *sqlx.DB) *HealthHandler { + return &HealthHandler{db: db} } func (h *HealthHandler) Health(c *gin.Context) { - c.Status(204) + _, error := h.db.Exec("SELECT 1") + if error != nil { + c.Status(http.StatusInternalServerError) + return + } + + c.Status(http.StatusNoContent) } diff --git a/internal/handlers/health_handler_test.go b/internal/handlers/health_handler_test.go index 1ae69ac..de20b6a 100644 --- a/internal/handlers/health_handler_test.go +++ b/internal/handlers/health_handler_test.go @@ -5,20 +5,46 @@ import ( "net/http/httptest" "testing" + "github.com/albertoadami/nestled/internal/testhelpers" "github.com/gin-gonic/gin" + _ "github.com/lib/pq" "github.com/stretchr/testify/assert" ) -func TestHealth(t *testing.T) { +func TestHealthHandlerSuccess(t *testing.T) { + // Start PostgreSQL container and get connection + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + + // set up Gin router and handler gin.SetMode(gin.TestMode) + router := gin.New() + healthHandler := NewHealthHandler(db) - r := gin.New() - h := NewHealthHandler() - r.GET("/health", h.Health) + router.GET("/health", healthHandler.Health) - req := httptest.NewRequest(http.MethodGet, "/health", nil) + req, _ := http.NewRequest("GET", "/health", nil) w := httptest.NewRecorder() - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } + +func TestHealthHandlerDBDown(t *testing.T) { + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + + // Simulate DB down + db.Close() + + gin.SetMode(gin.TestMode) + router := gin.New() + healthHandler := NewHealthHandler(db) + router.GET("/health", healthHandler.Health) + + req, _ := http.NewRequest("GET", "/health", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) +} diff --git a/internal/handlers/user_handler.go b/internal/handlers/user_handler.go new file mode 100644 index 0000000..a33e5d3 --- /dev/null +++ b/internal/handlers/user_handler.go @@ -0,0 +1,59 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/albertoadami/nestled/internal/dto" + "github.com/albertoadami/nestled/internal/errors" + "github.com/albertoadami/nestled/internal/services" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type UserHandler struct { + userService services.UserService + logger *zap.Logger +} + +func NewUserHandler(userService services.UserService, logger *zap.Logger) *UserHandler { + return &UserHandler{ + userService: userService, + logger: logger, + } +} + +func (u *UserHandler) RegisterUser(c *gin.Context) { + + var request dto.CreateUserRequest + + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + userId, err := u.userService.CreateUser(&request) + if err != nil { + switch err { + case errors.ErrUsernameAlreadyExists: + c.JSON(http.StatusConflict, dto.NewErrorResponse(err.Error(), fmt.Sprintf("The username %s is already in use", request.Username))) + return + case errors.ErrEmailAlreadyExists: + c.JSON(http.StatusConflict, dto.NewErrorResponse(err.Error(), fmt.Sprintf("The email %s is already in use", request.Email))) + return + case errors.ErrPasswordTooWeak: + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + default: + c.Status(500) + return + } + } + + u.logger.Info(fmt.Sprintf("User created with ID: %s", userId)) + + locationPathResponse := fmt.Sprintf("/api/v1/users/%s", userId) + c.Header("Location", locationPathResponse) + c.Status(http.StatusCreated) + +} diff --git a/internal/handlers/user_handler_test.go b/internal/handlers/user_handler_test.go new file mode 100644 index 0000000..cc9ce0f --- /dev/null +++ b/internal/handlers/user_handler_test.go @@ -0,0 +1,98 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/albertoadami/nestled/internal/dto" + "github.com/albertoadami/nestled/internal/errors" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +type mockUserService struct { + createUserFn func(req *dto.CreateUserRequest) (uuid.UUID, error) +} + +func (m *mockUserService) CreateUser(req *dto.CreateUserRequest) (uuid.UUID, error) { + return m.createUserFn(req) +} + +func setupUserRouter(mockService *mockUserService) *gin.Engine { + gin.SetMode(gin.TestMode) + router := gin.New() + handler := NewUserHandler(mockService, zap.NewNop()) + router.POST("/api/v1/users/register", handler.RegisterUser) + return router +} + +func createUserRequest() *http.Request { + body := `{"username":"test","email":"test@github.com","password":"secret123", "first_name":"Test","last_name":"User"}` + req, _ := http.NewRequest("POST", "/api/v1/users/register", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + return req +} + +func TestRegisterUserSuccessfully(t *testing.T) { + + userId := uuid.New() + + mockService := &mockUserService{ + createUserFn: func(req *dto.CreateUserRequest) (uuid.UUID, error) { + return userId, nil + }, + } + + router := setupUserRouter(mockService) + + req := createUserRequest() + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Fatalf("Expected status code 201, got %d", w.Code) + } + + location := w.Header().Get("Location") + expectedLocation := "/api/v1/users/" + userId.String() + assert.Equal(t, expectedLocation, location) + +} + +func TestRegisterUsernameAlreadyExists(t *testing.T) { + + mockService := &mockUserService{ + createUserFn: func(req *dto.CreateUserRequest) (uuid.UUID, error) { + return uuid.Nil, errors.ErrUsernameAlreadyExists + }, + } + + router := setupUserRouter(mockService) + + req := createUserRequest() + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusConflict, w.Code) +} + +func TestRegisterEmailAlreadyExists(t *testing.T) { + + mockService := &mockUserService{ + createUserFn: func(req *dto.CreateUserRequest) (uuid.UUID, error) { + return uuid.Nil, errors.ErrEmailAlreadyExists + }, + } + + router := setupUserRouter(mockService) + + req := createUserRequest() + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusConflict, w.Code) +} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..a695e03 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,23 @@ +package model + +import ( + "github.com/google/uuid" +) + +type UserStatus string + +const ( + UserStatusActive UserStatus = "ACTIVE" + UserStatusBlocked UserStatus = "BLOCKED" + UserStatusPending UserStatus = "PENDING" +) + +type User struct { + Id uuid.UUID + Username string + FirstName string + LastName string + Email string + PasswordHash string + Status UserStatus +} diff --git a/internal/repositories/user_repository.go b/internal/repositories/user_repository.go new file mode 100644 index 0000000..d5a9ca8 --- /dev/null +++ b/internal/repositories/user_repository.go @@ -0,0 +1,52 @@ +package repositories + +import ( + "github.com/albertoadami/nestled/internal/errors" + "github.com/albertoadami/nestled/internal/model" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +type UserRepository interface { + CreateUser(user *model.User) (uuid.UUID, error) +} + +type userRepository struct { + db *sqlx.DB +} + +func NewUserRepository(db *sqlx.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) CreateUser(user *model.User) (uuid.UUID, error) { + query := `INSERT INTO users (id, username, first_name, last_name, email, password_hash, status) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id` + + var id uuid.UUID + err := r.db.QueryRowx(query, + user.Id, + user.Username, + user.FirstName, + user.LastName, + user.Email, + user.PasswordHash, + user.Status, + ).Scan(&id) + + if err != nil { + if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { + switch pqErr.Constraint { + case "users_username_key": + return uuid.Nil, errors.ErrUsernameAlreadyExists + case "users_email_key": + return uuid.Nil, errors.ErrEmailAlreadyExists + } + } + return uuid.Nil, err + } + + return id, nil +} diff --git a/internal/repositories/user_repository_test.go b/internal/repositories/user_repository_test.go new file mode 100644 index 0000000..1204206 --- /dev/null +++ b/internal/repositories/user_repository_test.go @@ -0,0 +1,106 @@ +package repositories + +import ( + "errors" + "testing" + + apperrors "github.com/albertoadami/nestled/internal/errors" + "github.com/albertoadami/nestled/internal/model" + "github.com/albertoadami/nestled/internal/testhelpers" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" +) + +func createTestUser() *model.User { + return &model.User{ + Id: uuid.New(), + FirstName: "John", + LastName: "Doe", + Username: "johndoe", + Email: "test@test.it", + PasswordHash: "hashedpassword", + Status: model.UserStatusPending, + } +} + +func truncateUsers(t *testing.T, testDB *sqlx.DB) { + _, err := testDB.Exec("TRUNCATE TABLE users CASCADE") + if err != nil { + t.Fatalf("failed to truncate users: %v", err) + } +} + +func TestCreateUserSucessfully(t *testing.T) { + + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + truncateUsers(t, db) + + userRepo := NewUserRepository(db) + + user := createTestUser() + + id, err := userRepo.CreateUser(user) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if id != user.Id { + t.Fatalf("expected id %v, got %v", user.Id, id) + } + +} + +func TestCreateUserFailedDueToDuplicateUsername(t *testing.T) { + + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + + truncateUsers(t, db) + + userRepo := NewUserRepository(db) + + user := createTestUser() + _, err := userRepo.CreateUser(user) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + user.Email = "test-duplicated@test.it" + user.Id = uuid.New() + _, err = userRepo.CreateUser(user) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, apperrors.ErrUsernameAlreadyExists) { + t.Fatalf("expected error %v, got %v", apperrors.ErrUsernameAlreadyExists, err) + } +} + +func TestCreateUserFailedDueToDuplicateEmail(t *testing.T) { + + db, terminate := testhelpers.SetupPostgres(t) + defer terminate() + + truncateUsers(t, db) + + userRepo := NewUserRepository(db) + + user := createTestUser() + _, err := userRepo.CreateUser(user) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + user.Username = "johndoe-duplicated" + user.Id = uuid.New() + _, err = userRepo.CreateUser(user) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !errors.Is(err, apperrors.ErrEmailAlreadyExists) { + t.Fatalf("expected error %v, got %v", apperrors.ErrEmailAlreadyExists, err) + } +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 6a02248..35dcc15 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -5,6 +5,12 @@ import ( "github.com/gin-gonic/gin" ) -func SetupRoutes(r *gin.Engine, healthHandler *handlers.HealthHandler) { +const ApiPrefix = "/api/v1" + +func SetupRoutes(r *gin.Engine, userHandler *handlers.UserHandler, healthHandler *handlers.HealthHandler) { r.GET("/health", healthHandler.Health) + + apiGroup := r.Group(ApiPrefix) + apiGroup.POST("/register", userHandler.RegisterUser) + } diff --git a/internal/services/user_service.go b/internal/services/user_service.go new file mode 100644 index 0000000..404a225 --- /dev/null +++ b/internal/services/user_service.go @@ -0,0 +1,44 @@ +package services + +import ( + "github.com/albertoadami/nestled/internal/crypto" + "github.com/albertoadami/nestled/internal/dto" + "github.com/albertoadami/nestled/internal/model" + "github.com/albertoadami/nestled/internal/repositories" + "github.com/google/uuid" +) + +type UserService interface { + CreateUser(request *dto.CreateUserRequest) (uuid.UUID, error) +} + +type userService struct { + userRepository repositories.UserRepository +} + +func NewUserService(userRepository repositories.UserRepository) UserService { + return &userService{ + userRepository: userRepository, + } +} + +func (s *userService) CreateUser(request *dto.CreateUserRequest) (uuid.UUID, error) { + + hashedPassword, err := crypto.HashPassword(request.Password) + if err != nil { + return uuid.Nil, err + } + + user := &model.User{ + Id: uuid.New(), + FirstName: request.FirstName, + LastName: request.LastName, + Username: request.Username, + Email: request.Email, + PasswordHash: hashedPassword, + Status: model.UserStatusPending, + } + + return s.userRepository.CreateUser(user) + +} diff --git a/internal/testhelpers/postgres.go b/internal/testhelpers/postgres.go new file mode 100644 index 0000000..5d81ae1 --- /dev/null +++ b/internal/testhelpers/postgres.go @@ -0,0 +1,76 @@ +package testhelpers + +import ( + "context" + "fmt" + "testing" + + migrate "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +func SetupPostgres(t *testing.T) (*sqlx.DB, func()) { + ctx := context.Background() + req := testcontainers.ContainerRequest{ + Image: "postgres:17", + ExposedPorts: []string{"5432/tcp"}, + Env: map[string]string{ + "POSTGRES_USER": "testuser", + "POSTGRES_PASSWORD": "testpass", + "POSTGRES_DB": "testdb", + }, + WaitingFor: wait.ForListeningPort("5432/tcp"), + } + + postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + assert.NoError(t, err) + + host, err := postgres.Host(ctx) + assert.NoError(t, err) + portObj, err := postgres.MappedPort(ctx, "5432") + assert.NoError(t, err) + + dsn := fmt.Sprintf("host=%s port=%s user=testuser password=testpass dbname=testdb sslmode=disable", + host, portObj.Port()) + + db, err := sqlx.Connect("postgres", dsn) + assert.NoError(t, err) + + terminate := func() { + db.Close() + _ = postgres.Terminate(ctx) + } + + // run migrations + runMigrations(t, host, portObj.Port()) + + return db, terminate +} + +func runMigrations(t *testing.T, host, port string) { + dbURL := fmt.Sprintf("postgres://testuser:testpass@%s:%s/testdb?sslmode=disable", host, port) + + m, err := migrate.New( + "file://../../migrations", + dbURL, + ) + assert.NoError(t, err) + if m == nil { + t.Fatal("migrate instance is nil, check migrations path and db url") + } + defer m.Close() + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + assert.NoError(t, err) + } +} diff --git a/migrations/000002_add_users_name_surname.down.sql b/migrations/000002_add_users_name_surname.down.sql new file mode 100644 index 0000000..9333b43 --- /dev/null +++ b/migrations/000002_add_users_name_surname.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE users DROP COLUMN first_name; +ALTER TABLE users DROP COLUMN last_name; \ No newline at end of file diff --git a/migrations/000002_add_users_name_surname.up.sql b/migrations/000002_add_users_name_surname.up.sql new file mode 100644 index 0000000..e6a3db0 --- /dev/null +++ b/migrations/000002_add_users_name_surname.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN first_name VARCHAR(255) NOT NULL; +ALTER TABLE users ADD COLUMN last_name VARCHAR(255) NOT NULL; \ No newline at end of file