commit 83ce16b16fcd3cf8a8c7310b59909b323b31f1c9 Author: Nikolai Rodionov Date: Thu Apr 30 19:30:31 2026 +0200 Init commit Signed-off-by: Nikolai Rodionov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8d79a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +softplayer-backend \ No newline at end of file diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml new file mode 100644 index 0000000..ad51cc1 --- /dev/null +++ b/.woodpecker/build.yaml @@ -0,0 +1,23 @@ +when: + event: + - push + +steps: + - name: Build and push a container image + image: gitea.badhouseplants.net/badhouseplants/container-builder:latest + environment: + REGISTRY_TOKEN: + from_secret: GITEA_REGISTRY_TOKEN + privileged: true + commands: + - build-container + backend_options: + kubernetes: + resources: + requests: + memory: 500Mi + cpu: 200m + limits: + memory: 500Mi + securityContext: + privileged: true diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..49dcc94 --- /dev/null +++ b/Containerfile @@ -0,0 +1,14 @@ +FROM golang:1.26.2 +WORKDIR /app + +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -o backend + +FROM scratch +COPY --from=0 /app/backend /app +COPY --from=0 /etc/ssl /etc/ssl +COPY migrations /migrations +ENTRYPOINT ["/app"] diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..d9bfa02 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,40 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json + +version: "3" + +tasks: + build: + desc: Build go code + cmd: go build + silent: true + + run-migrations-dev: + desc: Execute database migrations + env: + SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable + cmd: go run main.go migrate --migrations-path=file://migrations + + run-server-dev: + desc: Run the local dev server + env: + SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable + SOFTPLAYER_REDIS_HOST: localhost:30379 + cmd: go run main.go serve --dev-mode --reflection + + deploy-local-env: + desc: Run a kind cluster and deploy deps + deps: + - kind-cluster + - helmfile-deploy + + kind-cluster: + desc: Run a kind cluster + cmd: kind create cluster --config ./kind-config.yaml + + kind-cluster-remove: + desc: Remove the kind cluster + cmd: kind delete cluster + + helmfile-deploy: + desc: Deploy the helmfile for the local dev + cmd: helmfile apply diff --git a/api/v1/accounts_auth.go b/api/v1/accounts_auth.go new file mode 100644 index 0000000..b3687c9 --- /dev/null +++ b/api/v1/accounts_auth.go @@ -0,0 +1,59 @@ +package v1 + +import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +func NewAccountAuthRPCImpl(ctrl *controllers.AccountController) *AccountsAuthServer { + return &AccountsAuthServer{ + ctrl: ctrl, + } +} + +type AccountsAuthServer struct { + accounts.UnimplementedAccountsAuthServiceServer + ctrl *controllers.AccountController +} + +func (a *AccountsAuthServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) { + tokenID := ctx.Value("token_id").(string) + userID := ctx.Value("user_id").(string) + log := logger.FromContext(ctx) + uuid, err := a.ctrl.ValidateRefreshToken(ctx, tokenID, userID) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "refresh token is invalid") + } + accessToken, err := a.ctrl.GenerateAccessToken(uuid) + if err != nil { + log.Error(err, "Couldn't generate an access token") + return nil, status.Error(codes.Aborted, "Couldn't generate Access Token") + } + + refreshToken, err := a.ctrl.GenerateRefreshToken(ctx, uuid) + if err != nil { + log.Error(err, "Couldn't generate a refresh token") + return nil, status.Error(codes.Aborted, "Couldn't generate Access Token") + } + + header := metadata.Pairs( + "access-token", accessToken, + "refreshToken", refreshToken, + ) + + if err := grpc.SetHeader(ctx, header); err != nil { + log.Error(err, "Couldn't set headers") + return nil, status.Error(codes.Unknown, "Couldn't set headers") + } + + return &emptypb.Empty{}, nil +} diff --git a/api/v1/accounts_no_auth.go b/api/v1/accounts_no_auth.go new file mode 100644 index 0000000..d6aad9f --- /dev/null +++ b/api/v1/accounts_no_auth.go @@ -0,0 +1,73 @@ +package v1 + +import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/golang/protobuf/ptypes/empty" + "golang.org/x/oauth2" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +func NewAccountNoAuthRPCImpl(ctrl *controllers.AccountController) *AccountsNoAuthServer { + return &AccountsNoAuthServer{ + ctrl: ctrl, + } +} + +type AccountsNoAuthServer struct { + accounts.UnimplementedAccountsNoAuthServiceServer + ctrl *controllers.AccountController +} + +func (a *AccountsNoAuthServer) SignIn(ctx context.Context, in *accounts.SignInRequest) (*empty.Empty, error) { + provider, err := oidc.NewProvider(ctx, "https://authentik.badhouseplants.net") + if err != nil { + return nil, err + } + + // Configure an OpenID Connect aware OAuth2 client. + oauth2Config := oauth2.Config{ + ClientID: "softplayer-localhost", + ClientSecret: "pRpe3scGUE2jNH6t5rqI9R4OROeQHs4eO6ku957mYjDumKhQGX8QJcO0BMJ2FG4sUpvFrqccEqWgc3wKMp94tC8LyvTnkPF0Tg0CaldAEHuoQQdNKAzXVxwrHE6kNyBC", + RedirectURL: "http://localhost:8080/#/auth/callback", + + // Discovery returns the OAuth2 endpoints. + Endpoint: provider.Endpoint(), + + // "openid" is a required scope for OpenID Connect flows. + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + } + verifier := provider.Verifier(&oidc.Config{ClientID: "softplayer-localhost"}) + + oauth2Token, err := oauth2Config.Exchange(ctx, in.Code) + if err != nil { + return nil, err + } + + // Extract the ID Token from OAuth2 token. + rawIDToken, ok := oauth2Token.Extra("id_token").(string) + if !ok { + return nil, status.Error(codes.Unauthenticated, "Couldn't parse oauth token") + } + + // Parse and verify ID Token payload. + idToken, err := verifier.Verify(ctx, rawIDToken) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "Couldn't verify oauth token") + } + + // Extract custom claims + var claims struct { + Email string `json:"email"` + Verified bool `json:"email_verified"` + } + if err := idToken.Claims(&claims); err != nil { + // handle error + } + return &emptypb.Empty{}, nil +} diff --git a/api/v1/test_auth.go b/api/v1/test_auth.go new file mode 100644 index 0000000..692b3de --- /dev/null +++ b/api/v1/test_auth.go @@ -0,0 +1,22 @@ +package v1 + +import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" +) + +func NewTestAuthRPCImpl() *TestAuthServer { + return &TestAuthServer{} +} + +type TestAuthServer struct { + test.UnimplementedTestAuthServiceServer +} + +func (t *TestAuthServer) Pong(ctx context.Context, in *test.PongRequest) (*test.PongResponse, error) { + log := logger.FromContext(ctx) + log.Info("Pong") + return &test.PongResponse{}, nil +} diff --git a/api/v1/test_no_auth.go b/api/v1/test_no_auth.go new file mode 100644 index 0000000..40bbd09 --- /dev/null +++ b/api/v1/test_no_auth.go @@ -0,0 +1,22 @@ +package v1 + +import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" +) + +func NewTestNoAuthRPCImpl() *TestNoAuthServer { + return &TestNoAuthServer{} +} + +type TestNoAuthServer struct { + test.UnimplementedTestNoAuthServiceServer +} + +func (t *TestNoAuthServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) { + log := logger.FromContext(ctx) + log.Info("Ping") + return &test.PingResponse{}, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..80ad433 --- /dev/null +++ b/go.mod @@ -0,0 +1,151 @@ +module gitea.badhouseplants.net/softplayer/softplayer-backend + +go 1.25.9 + +require ( + github.com/alecthomas/assert/v2 v2.11.0 + github.com/alecthomas/kong v1.15.0 + github.com/coreos/go-oidc/v3 v3.18.0 + github.com/go-logr/logr v1.4.3 + github.com/go-logr/zapr v1.3.0 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/golang-migrate/migrate/v4 v4.19.1 + github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/mattn/go-colorable v0.1.13 + github.com/redis/go-redis/v9 v9.18.0 + github.com/sirupsen/logrus v1.9.3 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.47.0 + golang.org/x/oauth2 v0.36.0 + gopkg.in/yaml.v2 v2.4.0 + helm.sh/helm/v3 v3.20.2 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + sigs.k8s.io/controller-runtime v0.23.3 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/alecthomas/repr v0.5.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.30 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/time v0.12.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/apiserver v0.35.1 // indirect + k8s.io/cli-runtime v0.35.1 // indirect + k8s.io/component-base v0.35.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kubectl v0.35.1 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +require ( + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0 + github.com/golang/protobuf v1.5.4 + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f9ec13b --- /dev/null +++ b/go.sum @@ -0,0 +1,563 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0 h1:2UggBAWgOJ1MYgkk+RTaWhfTGtzAZ0B9MriZMoHqnq4= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= +github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= +github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= +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/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= +github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +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/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +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/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +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.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +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/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= +google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48= +helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= +k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= +k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE= +k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= +k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/helmfile.yaml.gotmpl b/helmfile.yaml.gotmpl new file mode 100644 index 0000000..fe66f6d --- /dev/null +++ b/helmfile.yaml.gotmpl @@ -0,0 +1,73 @@ +environments: + default: + kubeContext: kind-kind + values: + - databases: + postgres: + installed: {{ env "POSTGRES_INSTALL" | default false }} + username: {{ env "POSTGRES_USER" | default "softplayer" }} + password: {{ env "POSTGRES_PASSWORD" | default "qwertyu9" }} + nodePort: 30432 + imageTag: {{ env "POSTGRES_VERSION" | default 18 }} +--- +repositories: + - name: cloudpirates + url: registry-1.docker.io/cloudpirates + oci: true + - name: dragonfly + url: ghcr.io/dragonflydb/dragonfly/helm + oci: true + +releases: + - name: postgres-instance + namespace: softplayer + chart: cloudpirates/postgres + version: 0.19.1 + installed: true + values: + - auth: + username: {{ .Values.databases.postgres.username }} + password: {{ .Values.databases.postgres.password }} + database: softplayer + - service: + type: NodePort + nodePort: {{ .Values.databases.postgres.nodePort }} + - persistentVolumeClaimRetentionPolicy: + enabled: true + whenDeleted: Delete + - name: dragonfly + namespace: softplayer + chart: dragonfly/dragonfly + version: v1.38.0 + installed: true + values: + - storage: + enabled: true + requests: 128Mi # Set a desired volume size for PVC. + extraArgs: + - --dbfilename=my-dump-{timestamp} # Only the filename without any file extensions. + - --snapshot_cron=* * * * * # Set a valid cron schedule. + podSecurityContext: + fsGroup: 2000 + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + service: + port: 30379 + type: NodePort + strategicMergePatches: + - apiVersion: v1 + kind: Service + metadata: + name: dragonfly + namespace: softplayer + spec: + ports: + - name: dragonfly + port: 30379 + protocol: TCP + nodePort: 30379 diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..f46b8a0 --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,18 @@ +package consts + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + USERNAME_LABEL_KEY = "username" + EMAIL_VERIFIED_LABEL_KEY = "email-verified" + EMAIL_VERIFIED_LABEL_TRUE = "true" + EMAIL_VERIFIED_LABEL_FALSE = "false" + SOFTPLAYER_ACCOUNTS_NAMESPACE = "softplayer-accounts" +) + +var ( + ErrSystemError = status.Error(codes.Internal, "a system error occured, we will try to fix it as soon as possible") +) diff --git a/internal/controllers/accounts.go b/internal/controllers/accounts.go new file mode 100644 index 0000000..df3e21d --- /dev/null +++ b/internal/controllers/accounts.go @@ -0,0 +1,104 @@ +package controllers + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/redis/go-redis/v9" +) + +type AccountController struct { + DB *sql.DB + Redis *redis.Client + DevMode bool + HashCost int16 + AccessTokenTTL time.Duration + RefreshTokenTTL time.Duration + JWTSecret []byte +} + +type JWT struct { + RefreshToken string + AccessToken string +} + +type AccountParams struct{} + +type AccountData struct { + Username string + Password string + Email string + UUID string +} + +func (c *AccountController) Create(ctx context.Context, data *AccountData) (string, error) { + data.UUID = uuid.New().String() + + passwordHash, err := hash.HashPassword(data.Password, int(c.HashCost)) + if err != nil { + return "", nil + } + + query := "INSERT INTO users (uuid, username, email, password_hash) VALUES ($1, $2, $3, $4)" + if _, err := c.DB.Query(query, data.UUID, data.Username, data.Email, passwordHash); err != nil { + return "", err + } + + return data.UUID, nil +} + +func (c *AccountController) GenerateAccessToken(userID string) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "type": "access", + "exp": time.Now().Add(c.AccessTokenTTL).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(c.JWTSecret) +} + +func redisKey(id string) string { + return fmt.Sprintf("refresh:%s", id) +} + +func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID string) (string, error) { + tokenID := uuid.New().String() + claims := jwt.MapClaims{ + "user_id": userID, + "token_id": tokenID, + "type": "refresh", + "exp": time.Now().Add(c.RefreshTokenTTL).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + if err := c.Redis.Set(ctx, redisKey(tokenID), userID, c.RefreshTokenTTL).Err(); err != nil { + return "", err + } + return token.SignedString(c.JWTSecret) +} + +// It must validate the refresh token +// Get it's id from the content +// Find a corresponding token in redis, and if it's found, remove it and create a new one +func (c *AccountController) ValidateRefreshToken(ctx context.Context, tokenID, userID string) (string, error) { + log := logger.FromContext(ctx) + userIDRedis := c.Redis.Get(ctx, redisKey(tokenID)).Val() + if err := c.Redis.Del(ctx, redisKey(tokenID)).Err(); err != nil { + log.Error(err, "Couldn't delete redis entry") + return "", err + } + log.Info(userIDRedis) + log.Info(userID) + if userID != userIDRedis { + return "", errors.New("user id doesn't match") + } + return userIDRedis, nil +} diff --git a/internal/controllers/applications.go b/internal/controllers/applications.go new file mode 100644 index 0000000..483c61b --- /dev/null +++ b/internal/controllers/applications.go @@ -0,0 +1,421 @@ +package controllers + +import ( + "bytes" + "context" + b64 "encoding/base64" + "fmt" + "os" + "strings" + "text/template" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/consts" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/helm" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/types/helmrelease" + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/google/uuid" + "go.uber.org/zap" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" +) + +type Application struct { + Controller ctrl.Manager + UserID string + Data *ApplicationData + Token string +} + +type ApplicationData struct { + UUID string + Name string + Description string + Application string + Version string + Environemnt string + Config map[string]string + RawConfig string +} + +// Create environment should create a new configmap in the user's namespace +// using a token that belongs to the user. +func (app *Application) Create(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + app.Data.UUID = uuid.New().String() + + app.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: app.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + controller, err := ctrl.NewManager(conf, ctrl.Options{}) + + if err != nil { + return err + } + + helmEntry := helm.NewHelm() + // TODO: It should be possible to use other repos + release := &helm.ReleaseData{ + Name: app.Data.Name, + Chart: app.Data.Application, + Version: app.Data.Version, + RepositoryURL: "oci://registry.badhouseplants.net/softplayer/helm", + RepositoryKind: "oci", + RepositoryName: "softplayer", + } + formattedName := strings.ToLower( + b64.StdEncoding.EncodeToString( + []byte(app.Data.Application + app.Data.Name + app.Data.Name + app.Data.Environemnt), + ), + )[0:20] + + goPath := os.TempDir() + "/softplayer/" + formattedName + if err := os.MkdirAll(goPath, 0777); err != nil { + return err + } + + path, err := helmEntry.PullChart(goPath, release) + if err != nil { + log.Error(err, "Couldn't pull the chart") + return consts.ErrSystemError + } + + prettyCfgSupport := true + cfgSchema := map[string]*helmrelease.PrettyConfigSchema{} + cfgSchemaPath, err := os.ReadFile(fmt.Sprintf("%s/%s/config.yaml", goPath, path)) + if err != nil { + log.Error(err, "Couldn't find the config file") + prettyCfgSupport = false + } else { + err = yaml.Unmarshal(cfgSchemaPath, cfgSchema) + if err != nil { + log.Error(err, "Couldn't parse the pretty config") + return err + } + } + + cfg := &helmrelease.HelmRelease{ + Helm: helmrelease.Helm{ + Release: app.Data.Name, + Chart: helmrelease.Chart{ + Name: app.Data.Application, + Version: app.Data.Version, + }, + Repo: helmrelease.Repo{ + URL: release.RepositoryURL, + Type: release.RepositoryKind, + }, + }, + Config: helmrelease.Config{}, + } + + if len(app.Data.Config) > 0 && prettyCfgSupport { + for key, val := range app.Data.Config { + value, ok := cfgSchema[key] + if !ok { + return fmt.Errorf("unsuported config entry: %s", key) + } + tmpl, err := template.New("prettyconfig").Parse(val) + if err != nil { + log.Error(err, "Coudln't build a tempalte for prettyconfig") + return consts.ErrSystemError + } + var tmplRes bytes.Buffer + if err := tmpl.Execute(&tmplRes, app.Data); err != nil { + log.Error(err, "Couldn't execute the prettyconfig template") + return consts.ErrSystemError + } + + cfg.Config.Pretty = append(cfg.Config.Pretty, helmrelease.PrettyConfig{ + Key: key, + Path: value.Path, + Value: tmplRes.String(), + }) + } + } else if len(app.Data.RawConfig) > 0 { + cfg.Config.Raw = app.Data.RawConfig + } + + cfgYaml, err := yaml.Marshal(cfg) + if err != nil { + log.Error(err, "Couldn't marshall a pretty config into a struct") + return consts.ErrSystemError + } + + appSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: formattedName[0:20], + Namespace: app.UserID, + Labels: map[string]string{ + "component": "install", + "kind": "action", + "environment": app.Data.Environemnt, + "uuid": app.Data.UUID, + }, + }, + StringData: map[string]string{ + "values.yaml": string(cfgYaml), + }, + } + + if err := kube.Create(ctx, controller.GetClient(), &appSecret, false); err != nil { + log.Error(err, "Couldn't create a configmap") + return consts.ErrSystemError + } + + return nil +} + +func (app *Application) Delete(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + app.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: app.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + clientset, err := kubernetes.NewForConfig(conf) + if err != nil { + log.Error(err, "Couldn't create a new clientset") + return consts.ErrSystemError + } + + configmaps, err := clientset.CoreV1().ConfigMaps(app.UserID).List(ctx, metav1.ListOptions{LabelSelector: fmt.Sprintf("uuid=%s", app.Data.UUID)}) + if err != nil { + log.Error(err, "Couldn't list configmaps") + return consts.ErrSystemError + } + for _, cm := range configmaps.Items { + if err := clientset.CoreV1().ConfigMaps(app.UserID).Delete(ctx, cm.GetName(), *metav1.NewDeleteOptions(100)); err != nil { + log.Error(err, "Couldn't remove configmap", "name", cm.GetName(), "namespace", cm.GetNamespace()) + return consts.ErrSystemError + } + } + + return nil +} + +// func (env *Environemnt) Update(ctx context.Context) error { +// if err := env.isNsVerified(ctx); err != nil { +// log.Println("Can't verify ns") +// return err +// } + +// env.Controller.GetClient() +// conf := &rest.Config{ +// Host: "https://kubernetes.default.svc.cluster.local:443", +// BearerToken: env.Token, +// TLSClientConfig: rest.TLSClientConfig{ +// Insecure: true, +// }, +// } + +// controller, err := ctrl.NewManager(conf, ctrl.Options{}) + +// if err != nil { +// return err +// } +// oldEnv := &Environemnt{ +// Controller: env.Controller, +// UserID: env.UserID, +// Token: env.Token, +// Data: &ApplicationData{ +// UUID: env.Data.UUID, +// }, +// } + +// if err := oldEnv.Get(ctx); err != nil { +// return err +// } + +// // Check whter immutable fields are changed + +// if oldEnv.Data.Provider != env.Data.Provider { +// return errors.New("provider can't be changed") +// } +// if oldEnv.Data.Location != env.Data.Location { +// return errors.New("location can't be changed") +// } + +// vars, err := env.Data.buildVars() +// if err != nil { +// return err +// } +// obj := corev1.ConfigMap{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: env.Data.UUID, +// Namespace: env.UserID, +// Labels: map[string]string{ +// "component": "bootstrap", +// "kind": "environment", +// }, +// }, +// Data: map[string]string{ +// "name": env.Data.Name, +// "description": env.Data.Description, +// "vars": vars, +// }, +// } + +// if err := kube.Update(ctx, controller.GetClient(), &obj); err != nil { +// return err +// } + +// return nil +// } +// func (*Environemnt) Delete(ctx context.Context) error { +// env.Controller.GetClient() +// conf := &rest.Config{ +// Host: "https://kubernetes.default.svc.cluster.local:443", +// BearerToken: env.Token, +// TLSClientConfig: rest.TLSClientConfig{ +// Insecure: true, +// }, +// } + +// controller, err := ctrl.NewManager(conf, ctrl.Options{}) + +// if err != nil { +// return err +// } +// obj := corev1.ConfigMap{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: env.Data.UUID, +// Namespace: env.UserID, +// Labels: map[string]string{ +// "component": "bootstrap", +// }, +// }, +// } +// if err := kube.Delete(ctx, controller.GetClient(), &obj, false); err != nil { +// return err +// } + +// return nil +// } + +// func (env *Environemnt) ListEnvs(ctx context.Context) ([]*Environemnt, error) { +// env.Controller.GetClient() +// conf := &rest.Config{ +// Host: "https://kubernetes.default.svc.cluster.local:443", +// BearerToken: env.Token, +// TLSClientConfig: rest.TLSClientConfig{ +// Insecure: true, +// }, +// } +// clientset, err := kubernetes.NewForConfig(conf) +// if err != nil { +// return nil, err +// } +// configmaps, err := clientset.CoreV1().ConfigMaps(env.UserID).List(ctx, metav1.ListOptions{LabelSelector: "kind=environment"}) +// if err != nil { +// return nil, err +// } + +// result := []*Environemnt{} +// for _, cm := range configmaps.Items { +// i := &Environemnt{} +// data := &ApplicationData{ +// UUID: cm.GetName(), +// } +// i.Token = env.Token +// i.UserID = env.UserID +// i.Data = data +// i.Controller = env.Controller +// if err := i.Get(ctx); err != nil { +// return nil, err +// } +// result = append(result, i) +// } +// return result, nil +// } + +// func (env *Environemnt) Get(ctx context.Context) error { +// env.Controller.GetClient() +// conf := &rest.Config{ +// Host: "https://kubernetes.default.svc.cluster.local:443", +// BearerToken: env.Token, +// TLSClientConfig: rest.TLSClientConfig{ +// Insecure: true, +// }, +// } +// clientset, err := kubernetes.NewForConfig(conf) +// if err != nil { +// return err +// } +// envData, err := clientset.CoreV1().ConfigMaps(env.UserID).Get(ctx, env.Data.UUID, metav1.GetOptions{}) +// if err != nil { +// return err +// } + +// res, err := godotenv.Unmarshal(envData.Data["vars"]) +// if err != nil { +// return err +// } + +// if val, ok := envData.Data["name"]; ok { +// env.Data.Name = val +// } else { +// env.Data.Name = "" +// } + +// if val, ok := envData.Data["description"]; ok { +// env.Data.Description = val +// } else { +// env.Data.Description = "" +// } + +// if val, ok := res["SP_PROVIDER"]; ok { +// env.Data.Provider = val +// } else { +// env.Data.Provider = "" +// } +// if val, ok := res["SP_KUBERNETES"]; ok { +// env.Data.Kubernetes = val +// } else { +// env.Data.Kubernetes = "" +// } +// if val, ok := res["SP_SERVER_TYPE"]; ok { +// env.Data.ServerType = val +// } else { +// env.Data.ServerType = "" +// } +// if val, ok := res["SP_SERVER_LOCATION"]; ok { +// env.Data.Location = val +// } else { +// env.Data.Location = "" +// } + +// return nil +// } diff --git a/internal/controllers/email.go b/internal/controllers/email.go new file mode 100644 index 0000000..a930176 --- /dev/null +++ b/internal/controllers/email.go @@ -0,0 +1,127 @@ +package controllers + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "io" + "log" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" + ctrl "sigs.k8s.io/controller-runtime" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +type EmailSvc struct { + Controller ctrl.Manager + Data EmailData + EmailConfig email.EmailConf + DevMode bool +} + +type EmailData struct { + UserID string + Code string +} + +func (svc *EmailSvc) SendVerification(ctx context.Context) error { + client := svc.Controller.GetClient() + userns := &corev1.Namespace{} + if err := client.Get(ctx, types.NamespacedName{ + Name: svc.Data.UserID, + }, userns); err != nil { + return err + } + + userName, ok := userns.Labels["username"] + if !ok { + return errors.New("user not found") + } + accountData := &corev1.Secret{} + if err := client.Get(ctx, types.NamespacedName{ + Namespace: "softplayer-accounts", + Name: userName, + }, accountData); err != nil { + return err + } + + if val, ok := userns.Labels["email-verified"]; ok && val == "true" { + return errors.New("email is already verified") + } + + number := encodeToString(6) + svc.Data.Code = number + if !svc.DevMode { + emailContent := "Subject: Softplayer verification code\r\n" + "\r\n" + fmt.Sprintf("Your verification code is %s", number) + email := string(accountData.Data["email"]) + if err := svc.EmailConfig.SendEmail(email, emailContent); err != nil { + return err + } + } + emailCode := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "email-verification-code", + Namespace: svc.Data.UserID, + }, + Data: map[string]string{ + "code": number, + }, + } + + if err := kube.Create(ctx, client, &emailCode, true); err != nil { + return err + } + return nil +} + +func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error { + client := svc.Controller.GetClient() + emailCode := &corev1.ConfigMap{} + if err := client.Get(ctx, types.NamespacedName{ + Namespace: svc.Data.UserID, + Name: "email-verification-code", + }, emailCode); err != nil { + return err + } + + if svc.Data.Code != emailCode.Data["code"] { + log.Println(svc.Data.Code) + log.Println(emailCode.Data["code"]) + return errors.New("wrong verification code") + } + if err := client.Delete(ctx, emailCode); err != nil { + return err + } + + userns := &corev1.Namespace{} + if err := client.Get(ctx, types.NamespacedName{ + Name: svc.Data.UserID, + }, userns); err != nil { + return err + } + + userns.Labels["email-verified"] = "true" + if err := client.Update(ctx, userns); err != nil { + return err + } + return nil +} + +func encodeToString(max int) string { + b := make([]byte, max) + n, err := io.ReadAtLeast(rand.Reader, b, max) + if n != max { + panic(err) + } + for i := 0; i < len(b); i++ { + b[i] = table[int(b[i])%len(table)] + } + return string(b) +} + +var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} diff --git a/internal/controllers/environments.go b/internal/controllers/environments.go new file mode 100644 index 0000000..c3b0835 --- /dev/null +++ b/internal/controllers/environments.go @@ -0,0 +1,413 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/google/uuid" + "github.com/joho/godotenv" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/consts" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" +) + +type Environemnt struct { + Controller ctrl.Manager + Config *rest.Config + UserID string + Data *EnvironemntData + Token string +} + +type EnvironemntData struct { + UUID string + Name string + Description string + Provider string + Kubernetes string + Location string + ServerType string + DiskSize int +} + +func (e *EnvironemntData) buildVars() (string, error) { + // Please make sure that the same variables are used by ansible + vars := fmt.Sprintf(`# -- Generated by the softplayer controller +SP_PROVIDER=%s +SP_KUBERNETES=%s +SP_SERVER_TYPE=%s +SP_SERVER_LOCATION=%s +SP_DISK_SIZE=%d`, + e.Provider, + e.Kubernetes, + e.ServerType, + e.Location, + e.DiskSize, + ) + + return vars, nil +} + +// Check whether used has passed the email verification +func (env *Environemnt) isNsVerified(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + clientset, err := kubernetes.NewForConfig(env.Config) + if err != nil { + log.Error(err, "Couldn't create a new clientset") + return consts.ErrSystemError + } + ns, err := clientset.CoreV1().Namespaces().Get(ctx, env.UserID, metav1.GetOptions{}) + if err != nil { + log.Error(err, "Couldn't get a user's namespace") + if k8serrors.IsNotFound(err) { + err := errors.New("user not found by ID") + return status.Error(codes.NotFound, err.Error()) + } + return consts.ErrSystemError + } + + val, ok := ns.GetLabels()["email-verified"] + if !ok || val == "false" { + return errors.New("user email is not verified, can't create an new env") + } + + return nil +} + +// Create environment should create a new configmap in the user's namespace +// using a token that belongs to the user. +func (env *Environemnt) Create(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + if err := env.isNsVerified(ctx); err != nil { + return status.Error(codes.Unauthenticated, err.Error()) + } + + // Prepare a new ID for a enironment + env.Data.UUID = uuid.New().String() + + env.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: env.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + controller, err := ctrl.NewManager(conf, ctrl.Options{}) + if err != nil { + log.Error(err, "Couldn't init a controller") + return consts.ErrSystemError + } + + vars, err := env.Data.buildVars() + if err != nil { + log.Error(err, "Couldn't build the environment's dotenv file", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + obj := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: env.Data.UUID, + Namespace: env.UserID, + Labels: map[string]string{ + "component": "bootstrap", + "kind": "environment", + }, + }, + Data: map[string]string{ + "name": env.Data.Name, + "description": env.Data.Description, + "vars": vars, + }, + } + if err := kube.Create(ctx, controller.GetClient(), &obj, false); err != nil { + log.Error(err, "Couln't create the environment's configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + return nil +} + +func (env *Environemnt) Update(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + env.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: env.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + controller, err := ctrl.NewManager(conf, ctrl.Options{}) + if err != nil { + log.Error(err, "Couldn't init a controller") + return consts.ErrSystemError + } + oldEnv := &Environemnt{ + Controller: env.Controller, + UserID: env.UserID, + Token: env.Token, + Data: &EnvironemntData{ + UUID: env.Data.UUID, + }, + } + + if err := oldEnv.Get(ctx); err != nil { + log.Error(err, "Couldn't get environment's configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + // Check whter immutable fields are changed + + if oldEnv.Data.Provider != env.Data.Provider { + return errors.New("provider can't be changed") + } + if oldEnv.Data.Location != env.Data.Location { + return errors.New("location can't be changed") + } + + vars, err := env.Data.buildVars() + if err != nil { + log.Error(err, "Couldn't build the environment's dotenv file", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + obj := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: env.Data.UUID, + Namespace: env.UserID, + Labels: map[string]string{ + "component": "bootstrap", + "kind": "environment", + }, + }, + Data: map[string]string{ + "name": env.Data.Name, + "description": env.Data.Description, + "vars": vars, + }, + } + + if err := kube.Update(ctx, controller.GetClient(), &obj); err != nil { + log.Error(err, "Couln't update the environment's configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + return nil +} +func (env *Environemnt) Delete(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + env.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: env.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + controller, err := ctrl.NewManager(conf, ctrl.Options{}) + if err != nil { + log.Error(err, "couldn't init a controller") + return consts.ErrSystemError + } + obj := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: env.Data.UUID, + Namespace: env.UserID, + Labels: map[string]string{ + "component": "bootstrap", + }, + }, + } + if err := kube.Delete(ctx, controller.GetClient(), &obj, false); err != nil { + log.Error(err, "Couln't remove environment's configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + return nil +} + +func (env *Environemnt) List(ctx context.Context, searchString string) ([]*Environemnt, error) { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + env.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: env.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + clientset, err := kubernetes.NewForConfig(conf) + if err != nil { + log.Error(err, "Couldn't create a new clientset") + return nil, consts.ErrSystemError + } + configmaps, err := clientset.CoreV1().ConfigMaps(env.UserID).List(ctx, metav1.ListOptions{LabelSelector: "kind=environment"}) + if err != nil { + log.Error(err, "Couldn't list configmaps") + return nil, consts.ErrSystemError + } + + result := []*Environemnt{} + for _, cm := range configmaps.Items { + i := &Environemnt{} + data := &EnvironemntData{ + UUID: cm.GetName(), + } + i.Token = env.Token + i.UserID = env.UserID + i.Data = data + i.Controller = env.Controller + if err := i.Get(ctx); err != nil { + log.Error(err, "Couldn't get an environment", "environment_id", i.Data.UUID) + return nil, consts.ErrSystemError + } + if len(searchString) > 0 { + if strings.Contains(i.Data.Name, searchString) { + result = append(result, i) + } + if strings.Contains(i.Data.Description, searchString) { + result = append(result, i) + } + } else { + result = append(result, i) + } + } + return result, nil +} + +func (env *Environemnt) Get(ctx context.Context) error { + log, err := logr.FromContext(ctx) + if err != nil { + zapLog, err := zap.NewDevelopment() + if err != nil { + panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) + } + log = zapr.NewLogger(zapLog) + } + + env.Controller.GetClient() + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: env.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + clientset, err := kubernetes.NewForConfig(conf) + if err != nil { + log.Error(err, "Couldn't create a new clientset") + return consts.ErrSystemError + } + envData, err := clientset.CoreV1().ConfigMaps(env.UserID).Get(ctx, env.Data.UUID, metav1.GetOptions{}) + if err != nil { + log.Error(err, "Couldn't get an environment's configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + res, err := godotenv.Unmarshal(envData.Data["vars"]) + if err != nil { + log.Error(err, "Couldn't parse environment's dotenv file from a configmap", "environment_id", env.Data.UUID) + return consts.ErrSystemError + } + + if val, ok := envData.Data["name"]; ok { + env.Data.Name = val + } else { + env.Data.Name = "" + } + + if val, ok := envData.Data["description"]; ok { + env.Data.Description = val + } else { + env.Data.Description = "" + } + + if val, ok := res["SP_PROVIDER"]; ok { + env.Data.Provider = val + } else { + env.Data.Provider = "" + } + if val, ok := res["SP_KUBERNETES"]; ok { + env.Data.Kubernetes = val + } else { + env.Data.Kubernetes = "" + } + if val, ok := res["SP_SERVER_TYPE"]; ok { + env.Data.ServerType = val + } else { + env.Data.ServerType = "" + } + if val, ok := res["SP_SERVER_LOCATION"]; ok { + env.Data.Location = val + } else { + env.Data.Location = "" + } + if val, ok := res["SP_DISK_SIZE"]; ok { + intVal, err := strconv.Atoi(val) + if err != nil { + log.Error(err, "Couldn't parse disk size") + intVal = 0 + } + env.Data.DiskSize = intVal + } else { + env.Data.Location = "" + } + + return nil +} diff --git a/internal/helpers/email/email.go b/internal/helpers/email/email.go new file mode 100644 index 0000000..a890622 --- /dev/null +++ b/internal/helpers/email/email.go @@ -0,0 +1,22 @@ +package email + +import ( + "net/smtp" +) + +type EmailConf struct { + From string + Password string + SmtpHost string + SmtpPort string +} + +func (e *EmailConf) SendEmail(to string, message string) error { + messageByte := []byte(message) + auth := smtp.PlainAuth("", e.From, e.Password, e.SmtpHost) + + if err := smtp.SendMail(e.SmtpHost+":"+e.SmtpPort, auth, e.From, []string{to}, messageByte); err != nil { + return err + } + return nil +} diff --git a/internal/helpers/hash/hash.go b/internal/helpers/hash/hash.go new file mode 100644 index 0000000..2600575 --- /dev/null +++ b/internal/helpers/hash/hash.go @@ -0,0 +1,12 @@ +package hash + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string, cost int) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) error { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} diff --git a/internal/helpers/hash/hash_test.go b/internal/helpers/hash/hash_test.go new file mode 100644 index 0000000..2714721 --- /dev/null +++ b/internal/helpers/hash/hash_test.go @@ -0,0 +1,21 @@ +package hash_test + +import ( + "testing" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" + "github.com/alecthomas/assert/v2" +) + +func TestHashValid(t *testing.T) { + password := "qwertyu9" + hpass, err := hash.HashPassword(password, 10) + assert.NoError(t, err) + assert.NoError(t, hash.CheckPasswordHash(password, hpass)) +} + +func TestHashInvalid(t *testing.T) { + password := "qwertyu9" + invhash := "qwertyu9" + assert.Error(t, hash.CheckPasswordHash(password, invhash)) +} diff --git a/internal/helpers/helm/helm.go b/internal/helpers/helm/helm.go new file mode 100644 index 0000000..54a7e57 --- /dev/null +++ b/internal/helpers/helm/helm.go @@ -0,0 +1,178 @@ +package helm + +import ( + "fmt" + "os" + + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/engine" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/repo" +) + +type Helm struct{} + +func NewHelm() Helmhelper { + return &Helm{} +} + +func getDownloadDirPath(workdirPath string) string { + return fmt.Sprintf("%s/.charts", workdirPath) +} + +func getChartDirPath(downloadDirPath string, release *ReleaseData) string { + return fmt.Sprintf("%s/%s-%s-%s", downloadDirPath, release.RepositoryName, release.Chart, release.Version) + +} + +func (h *Helm) PullChart(workdirPath string, release *ReleaseData) (path string, err error) { + downloadDirPath := getDownloadDirPath(workdirPath) + if err := os.MkdirAll(downloadDirPath, 0777); err != nil { + return "", err + } + + config := new(action.Configuration) + cl := cli.New() + chartDir := getChartDirPath(downloadDirPath, release) + _, err = os.Stat(chartDir) + + if err != nil && !os.IsNotExist(err) { + return "", nil + } else if os.IsNotExist(err) { + if err := os.Mkdir(chartDir, 0777); err != nil { + return "", err + } + registry, err := registry.NewClient() + if err != nil { + return "", err + } + + var path string + // Download the chart to the workdir + if release.RepositoryKind != "oci" { + r, err := repo.NewChartRepository(&repo.Entry{ + Name: release.RepositoryName, + URL: release.RepositoryURL, + }, getter.All(cl)) + if err != nil { + return "", err + } + path = r.Config.Name + + } else { + path = release.RepositoryURL + } + + client := action.NewPullWithOpts(action.WithConfig(config)) + + client.Untar = true + client.UntarDir = workdirPath + client.SetRegistryClient(registry) + client.DestDir = workdirPath + client.Settings = cl + + chartRemote := fmt.Sprintf("%s/%s", path, release.Chart) + logrus.Infof("trying to pull: %s", chartRemote) + if _, err = client.Run(chartRemote); err != nil { + return "", err + } + } + return release.Chart, nil +} + +func (h *Helm) FindLatestVersion(workdirPath string, release *ReleaseData) (version string, err error) { + downloadDirPath := getDownloadDirPath(workdirPath) + if err := os.MkdirAll(downloadDirPath, 0777); err != nil { + return "", err + } + + config := new(action.Configuration) + cl := cli.New() + chartDir := getChartDirPath(downloadDirPath, release) + chartPath, err := h.PullChart(workdirPath, release) + if err != nil { + return "", err + } + + showAction := action.NewShowWithConfig(action.ShowChart, config) + + res, err := showAction.LocateChart(fmt.Sprintf("%s/%s", chartDir, chartPath), cl) + if err != nil { + return "", err + } + res, err = showAction.Run(res) + if err != nil { + return "", nil + } + + chartData, err := chartFromString(res) + if err != nil { + return "", err + } + logrus.Infof("the latest version of %s is %s", release.Chart, chartData.Version) + versionedChartDir := getChartDirPath(downloadDirPath, release) + os.Rename(chartDir, versionedChartDir) + return chartData.Version, err +} + +func (h *Helm) RenderChart(workdirPath string, release *ReleaseData) error { + downloadDirPath := getDownloadDirPath(workdirPath) + chartDirPath := getChartDirPath(downloadDirPath, release) + chartPath, err := getChartPathFromDir(chartDirPath) + if err != nil { + return err + } + logrus.Info(fmt.Sprintf("%s/%s", chartDirPath, chartPath)) + chartObj, err := loader.Load(fmt.Sprintf("%s/%s", chartDirPath, chartPath)) + if err != nil { + return err + } + values := chartutil.Values{} + values["Values"] = chartObj.Values + values["Release"] = map[string]string{ + "Name": release.Name, + "Namespace": release.Namespace, + } + values["Capabilities"] = map[string]map[string]string{ + "KubeVersion": { + "Version": "v1.27.9", + "GitVersion": "v1.27.9", + }, + } + files, err := engine.Engine{Strict: false}.Render(chartObj, values) + if err != nil { + return err + } + logrus.Info(files) + for file, data := range files { + logrus.Infof("%s - %s", file, data) + } + logrus.Info("I'm here") + return nil +} + +func getChartPathFromDir(downloadDir string) (file string, err error) { + files, err := os.ReadDir(downloadDir) + if err != nil { + return "", err + } else if len(files) == 0 { + return "", fmt.Errorf("expected to have one file, got zero in a dir %s", downloadDir) + } else if len(files) > 1 { + return "", fmt.Errorf("expected to have only one file in a dir %s", downloadDir) + } + return files[0].Name(), nil +} + +func chartFromString(info string) (*ReleaseData, error) { + releaseData := new(ReleaseData) + if err := yaml.Unmarshal([]byte(info), &releaseData); err != nil { + return nil, err + } + return releaseData, nil +} diff --git a/internal/helpers/helm/types.go b/internal/helpers/helm/types.go new file mode 100644 index 0000000..47960fa --- /dev/null +++ b/internal/helpers/helm/types.go @@ -0,0 +1,18 @@ +package helm + +type Helmhelper interface { + FindLatestVersion(workdirPath string, release *ReleaseData) (string, error) + PullChart(workdirPath string, release *ReleaseData) (string, error) + RenderChart(workdirPath string, release *ReleaseData) error +} + +type ReleaseData struct { + Name string + Chart string + Namespace string + Version string + RepositoryName string + RepositoryURL string + RepositoryKind string + ValuesData string +} diff --git a/internal/helpers/kube/kube.go b/internal/helpers/kube/kube.go new file mode 100644 index 0000000..1db4cac --- /dev/null +++ b/internal/helpers/kube/kube.go @@ -0,0 +1,68 @@ +package kube + +import ( + "context" + "fmt" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Create(ctx context.Context, client client.Client, obj client.Object, wait bool) error { + if err := client.Create(ctx, obj); err != nil { + return err + } + if wait { + if err := WaitUntilCreated(ctx, client, obj, 10, time.Millisecond*50); err != nil { + return err + } + } + return nil +} + +func Update(ctx context.Context, client client.Client, obj client.Object) error { + if err := client.Update(ctx, obj); err != nil { + return err + } + return nil +} + +func SetOwnerRef(ctx context.Context, client client.Client, obj client.Object, owner client.Object) client.Object { + apiVersion := fmt.Sprintf("%s/%s", owner.GetObjectKind().GroupVersionKind().Group, owner.GetObjectKind().GroupVersionKind().Version) + ownerReference := []metav1.OwnerReference{ + { + APIVersion: apiVersion, + Kind: owner.GetObjectKind().GroupVersionKind().GroupKind().Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + }, + } + obj.SetOwnerReferences(ownerReference) + return obj +} + +func WaitUntilCreated(ctx context.Context, client client.Client, obj client.Object, attemps int, timeout time.Duration) error { + if err := client.Get(ctx, types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, obj); err != nil { + if attemps > 0 { + time.Sleep(timeout) + if err := WaitUntilCreated(ctx, client, obj, attemps-1, timeout); err != nil { + return err + } + } else { + return err + } + } + return nil +} + +func Delete(ctx context.Context, client client.Client, obj client.Object, wait bool) error { + if err := client.Delete(ctx, obj); err != nil { + return err + } + return nil +} diff --git a/internal/helpers/kube/kube_test.go b/internal/helpers/kube/kube_test.go new file mode 100644 index 0000000..d283e01 --- /dev/null +++ b/internal/helpers/kube/kube_test.go @@ -0,0 +1 @@ +package kube_test diff --git a/internal/interceptors/authjwt.go b/internal/interceptors/authjwt.go new file mode 100644 index 0000000..30eca99 --- /dev/null +++ b/internal/interceptors/authjwt.go @@ -0,0 +1,64 @@ +package interceptors + +import ( + "context" + "fmt" + "strings" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + "github.com/golang-jwt/jwt/v5" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type JWTVerifier struct { + secret []byte + serverCtx context.Context +} + +func NewJWTVerifier(ctx context.Context, secret []byte) *JWTVerifier { + return &JWTVerifier{ + serverCtx: ctx, + secret: secret, + } +} + +// This is an interceptors that should verify that a user is authorized +func (v *JWTVerifier) JWTAuthInterceptor( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (interface{}, error) { + log := logger.FromContext(v.serverCtx).WithValues("method", info.FullMethod) + if !strings.Contains(info.FullMethod, "NoAuth") { + log.Info("Checking the JWT token") + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "User is not authorized") + } + + tokenString := md.Get("token")[0] + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") + return v.secret, nil + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "User is not authorized") + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + fmt.Println(claims["userID"]) + } else { + fmt.Println(err) + } + // Get the token from the metadata + // Validate the token + // Get the user id from the token + } else { + log.Info("Auth is not required for this request") + } + return handler(ctx, req) +} diff --git a/internal/providers/infra/common.go b/internal/providers/infra/common.go new file mode 100644 index 0000000..9b8704c --- /dev/null +++ b/internal/providers/infra/common.go @@ -0,0 +1,25 @@ +package infra + +import ( + "fmt" + + proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1" +) + +type Providers interface { + GetProviderName() string + RawProviderName() string + GetServerType(string) (string, error) + GetServerLocation(string) (string, error) + RawServerType(string) string + RawServerLocation(string) string +} + +func GetProvider(provider string) (Providers, error) { + switch provider { + case proto.Provider_PROVIDER_HETZNER.String(), "hetzner": + return &Hetzner{}, nil + default: + return nil, fmt.Errorf("unknown provider: %s", provider) + } +} diff --git a/internal/providers/infra/hetzner.go b/internal/providers/infra/hetzner.go new file mode 100644 index 0000000..a337339 --- /dev/null +++ b/internal/providers/infra/hetzner.go @@ -0,0 +1,100 @@ +package infra + +import ( + "errors" + "fmt" + "strings" + + proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1" +) + +type Hetzner struct{} + +// GetProviderName implements Providers. +func (h *Hetzner) GetProviderName() string { + return "hetzner" +} + +// RawProviderName implements Providers. +func (h *Hetzner) RawProviderName() string { + return proto.Provider_PROVIDER_HETZNER.String() +} + +// RawServerLocation implements Providers. +func (h *Hetzner) RawServerLocation(location string) string { + switch location { + case "ash": + return proto.Location_LOCATION_HETZNER_ASHBURN.String() + case "hil": + return proto.Location_LOCATION_HETZNER_HILLSBORO.String() + case "fsn1": + return proto.Location_LOCATION_HETZNER_FALKENSTEIN.String() + case "nbg1": + return proto.Location_LOCATION_HETZNER_NUREMBERG.String() + case "hel1": + return proto.Location_LOCATION_HETZNER_HELSINKI.String() + default: + return proto.Location_LOCATION_UNSPECIFIED.String() + } + +} + +// RawServerType implements Providers. +func (h *Hetzner) RawServerType(kind string) string { + switch kind { + case "cpx21": + return proto.ServerType_SERVER_TYPE_STARTER.String() + case "cpx31": + return proto.ServerType_SERVER_TYPE_REGULAR.String() + case "cpx41": + return proto.ServerType_SERVER_TYPE_PLUS.String() + case "cpx51": + return proto.ServerType_SERVER_TYPE_PRO.String() + default: + return proto.ServerType_SERVER_TYPE_UNSPECIFIED.String() + } +} + +// GetServerLocation implements Providers. +func (h *Hetzner) GetServerLocation(location string) (string, error) { + if !strings.Contains(location, "HETZNER") { + return "", fmt.Errorf("location isn't supported by hetzner: %s", location) + } + switch location { + case proto.Location_LOCATION_HETZNER_ASHBURN.String(): + return "ash", nil + case proto.Location_LOCATION_HETZNER_HILLSBORO.String(): + return "hil", nil + case proto.Location_LOCATION_HETZNER_FALKENSTEIN.String(): + return "fsn1", nil + case proto.Location_LOCATION_HETZNER_NUREMBERG.String(): + return "nbg1", nil + case proto.Location_LOCATION_HETZNER_HELSINKI.String(): + return "hel1", nil + default: + return "", fmt.Errorf("unknown location: %s", location) + } +} + +func (h *Hetzner) GetServerType(kind string) (serverType string, err error) { + switch kind { + case proto.ServerType_SERVER_TYPE_STARTER.String(): + serverType = "cpx21" + return + case proto.ServerType_SERVER_TYPE_REGULAR.String(): + serverType = "cpx31" + return + case proto.ServerType_SERVER_TYPE_PLUS.String(): + serverType = "cpx41" + return + case proto.ServerType_SERVER_TYPE_PRO.String(): + serverType = "cpx51" + return + case proto.ServerType_SERVER_TYPE_CUSTOM.String(): + err = errors.New("custom server types are not supported yet") + return + default: + err = fmt.Errorf("unknown server type: %s", kind) + return + } +} diff --git a/internal/providers/kubernetes/common.go b/internal/providers/kubernetes/common.go new file mode 100644 index 0000000..49943f3 --- /dev/null +++ b/internal/providers/kubernetes/common.go @@ -0,0 +1,21 @@ +package kubernetes + +import ( + "fmt" + + proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1" +) + +type Kubernetes interface { + GetKubernetesName() string + RawKubernetesName() string +} + +func GetKubernetes(k8s string) (Kubernetes, error) { + switch k8s { + case proto.Kubernetes_KUBERNETES_K3S.String(), "k3s": + return &K3s{}, nil + default: + return nil, fmt.Errorf("unknown provider: %s", k8s) + } +} diff --git a/internal/providers/kubernetes/k3s.go b/internal/providers/kubernetes/k3s.go new file mode 100644 index 0000000..75ab3fb --- /dev/null +++ b/internal/providers/kubernetes/k3s.go @@ -0,0 +1,15 @@ +package kubernetes + +import ( + proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1" +) + +type K3s struct{} + +func (k *K3s) GetKubernetesName() string { + return "k3s" +} + +func (k *K3s) RawKubernetesName() string { + return proto.Kubernetes_KUBERNETES_K3S.String() +} diff --git a/internal/tools/logger/logger.go b/internal/tools/logger/logger.go new file mode 100644 index 0000000..fbd21f7 --- /dev/null +++ b/internal/tools/logger/logger.go @@ -0,0 +1,45 @@ +package logger + +import ( + "context" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "github.com/mattn/go-colorable" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func SetupLogger(levelTxt string) *zap.Logger { + level, err := zapcore.ParseLevel(levelTxt) + if err != nil { + level = zapcore.ErrorLevel + } + aa := zap.NewDevelopmentEncoderConfig() + aa.EncodeLevel = zapcore.CapitalColorLevelEncoder + aa.TimeKey = "" + bb := zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(aa), + zapcore.AddSync(colorable.NewColorableStdout()), + level, + )) + return bb +} + +func NewLogger(ctx context.Context, levelTxt string) context.Context { + log := zapr.NewLogger(SetupLogger(levelTxt)) + ctx = logr.NewContext(ctx, log) + return ctx +} + +func FromContext(ctx context.Context) logr.Logger { + log, err := logr.FromContext(ctx) + if err == nil { + return log + } + return zapr.NewLogger(zap.NewExample()) +} + +func ToContext(ctx context.Context, logger logr.Logger) context.Context { + return logr.NewContext(ctx, logger) +} diff --git a/internal/types/helmrelease/helmrelease.go b/internal/types/helmrelease/helmrelease.go new file mode 100644 index 0000000..280cdf2 --- /dev/null +++ b/internal/types/helmrelease/helmrelease.go @@ -0,0 +1,37 @@ +package helmrelease + +type Chart struct { + Name string + Version string +} + +type Repo struct { + URL string + Type string +} + +type PrettyConfig struct { + Key string + Path string + Value string +} + +type Helm struct { + Release string + Chart Chart + Repo Repo +} + +type Config struct { + Pretty []PrettyConfig + Raw string +} +type HelmRelease struct { + Helm Helm + Config Config +} + +type PrettyConfigSchema struct { + Description string + Path string +} diff --git a/kind-config.yaml b/kind-config.yaml new file mode 100644 index 0000000..58a1f51 --- /dev/null +++ b/kind-config.yaml @@ -0,0 +1,9 @@ +apiVersion: kind.x-k8s.io/v1alpha4 +kind: Cluster +nodes: + - role: control-plane + extraPortMappings: + - containerPort: 30432 + hostPort: 30432 + - containerPort: 30379 + hostPort: 30379 diff --git a/main.go b/main.go new file mode 100644 index 0000000..cee7ddd --- /dev/null +++ b/main.go @@ -0,0 +1,224 @@ +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net" + "strings" + "time" + + v1 "gitea.badhouseplants.net/softplayer/softplayer-backend/api/v1" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" + accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" + test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" + "github.com/alecthomas/kong" + "github.com/golang-jwt/jwt/v5" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector" + _ "github.com/lib/pq" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/status" + + "github.com/redis/go-redis/v9" +) + +var CLI struct { + Serve Serve `cmd:"" help:"Start the grpc server"` + Migrate Migrate `cmd:"" help:"Run the database migrations"` +} + +type Serve struct { + // Service related + Port int64 `short:"p" env:"SOFTPLAYER_PORT" default:"4020"` + Host string `env:"SOFTPLAYER_HOST" default:"0.0.0.0"` + HashCost int16 `env:"SOFTPLAYER_HASH_COST" default:"1"` + // SMTP Config + SMTPHost string `env:"SOFTPLAYER_SMTP_HOST"` + SMTPPort string `env:"SOFTPLAYER_SMTP_PORT" default:"587"` + SMTPFrom string `env:"SOFTPLAYER_SMTP_FROM" default:"overlord@badhouseplants.net"` + SMTPPassword string `env:"SOFTPLAYER_SMTP_PASSWORD"` + // Database and redis + DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"` + RedisHost string `env:"SOFTPLAYER_REDIS_HOST"` + // JWT parameters + RefrestTokenTTL time.Duration `default:"8h"` + AccessTokenTTL time.Duration `default:"15m"` + JWTSecret string `default:"qwertyu9"` + // Dev and logging + Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"` + DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"` +} + +type Migrate struct { + DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"` + MigrationsPath string `env:"SOFTPLAYER_DB_MIGRATIOON_PATH" default:"file://migrations"` +} + +func main() { + kongCtx := kong.Parse(&CLI) + ctx := logger.NewLogger(context.Background(), "info") + switch kongCtx.Command() { + case "serve": + if err := server(ctx, CLI.Serve); err != nil { + panic(err) + } + case "migrate": + if err := migrateDB(ctx, CLI.Migrate); err != nil { + panic(err) + } + default: + panic(kongCtx.Command()) + } +} + +// Migrate the database to the latest version +func migrateDB(ctx context.Context, params Migrate) error { + log := logger.FromContext(ctx) + log.Info("Starting a database migration driver") + db, err := sql.Open("postgres", params.DBConnectionString) + if err != nil { + log.Error(err, "Couldn't start a database driver") + return err + } + + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + log.Error(err, "Couldn't start a database migration driver") + return err + } + + log.Info("Preparing database migrations") + m, err := migrate.NewWithDatabaseInstance( + params.MigrationsPath, + "postgres", driver) + if err != nil { + log.Error(err, "Couldn't perform database migrations") + return err + } + + log.Info("Starting database migrations") + err = m.Up() + if err != nil { + if errors.Is(err, migrate.ErrNoChange) { + log.Info("Database is already up-to-date") + } else { + log.Error(err, "Couldn't migrate the database") + return err + } + } + + return nil +} + +// Run the grpc backend server +func server(ctx context.Context, params Serve) error { + // Make sure the download dir exists + log := logger.FromContext(ctx) + + // Init a logger + //log.Info("Starting a kubernetes manager") + //controller, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{}) + //if err != nil { + // log.Error(err, "Coulnd't start a kube manager") + // return err + //} + + // TODO: Handle the error + //go func() { + // if err := controller.Start(ctx); err != nil { + // panic(err) + // } + //}() + + log.Info("Opening a database connection") + + db, err := sql.Open("postgres", params.DBConnectionString) + if err != nil { + log.Error(err, "Couldn't start a database driver") + return err + } + + //emailConfig := email.EmailConf{ + // From: params.SmtpFrom, + // Password: params.SmtpPassword, + // SmtpHost: params.SmtpHost, + // SmtpPort: params.SmtpPort, + //} + + address := fmt.Sprintf("%s:%d", params.Host, params.Port) + lis, err := net.Listen("tcp", address) + if err != nil { + return err + } + + // jwtVerifier := interceptors.NewJWTVerifier(ctx, []byte(params.JWTSecret)) + + authFn := func(ctx context.Context) (context.Context, error) { + tokenString, err := auth.AuthFromMD(ctx, "bearer") + if err != nil { + return nil, err + } + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { + return []byte(params.JWTSecret), nil + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "User is not authorized") + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + ctx = context.WithValue(ctx, "token_id", claims["token_id"].(string)) + ctx = context.WithValue(ctx, "user_id", claims["user_id"].(string)) + } else { + return ctx, errors.New("claims are missing int the token") + } + return ctx, nil + } + authReqServices := func(ctx context.Context, callMeta interceptors.CallMeta) bool { + return !strings.Contains(callMeta.Service, "NoAuth") + } + grpcServer := grpc.NewServer( + grpc.ChainUnaryInterceptor( + grpc_zap.UnaryServerInterceptor(logger.SetupLogger("info")), + // jwtVerifier.JWTAuthInterceptor, + selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(authReqServices)), + ), + grpc.StreamInterceptor(grpc_zap.StreamServerInterceptor(logger.SetupLogger("info"))), + ) + + rdb := redis.NewClient(&redis.Options{ + Addr: params.RedisHost, + }) + if params.Reflection { + reflection.Register(grpcServer) + } + + accountCtrl := &controllers.AccountController{ + HashCost: params.HashCost, + DB: db, + DevMode: params.DevMode, + RefreshTokenTTL: params.RefrestTokenTTL, + AccessTokenTTL: params.AccessTokenTTL, + JWTSecret: []byte(params.JWTSecret), + Redis: rdb, + } + accounts.RegisterAccountsNoAuthServiceServer(grpcServer, v1.NewAccountNoAuthRPCImpl(accountCtrl)) + accounts.RegisterAccountsAuthServiceServer(grpcServer, v1.NewAccountAuthRPCImpl(accountCtrl)) + test.RegisterTestAuthServiceServer(grpcServer, v1.NewTestAuthRPCImpl()) + test.RegisterTestNoAuthServiceServer(grpcServer, v1.NewTestNoAuthRPCImpl()) + if err := grpcServer.Serve(lis); err != nil { + return err + } + + return nil +} diff --git a/migrations/20260425183515_init.down.sql b/migrations/20260425183515_init.down.sql new file mode 100644 index 0000000..c99ddcd --- /dev/null +++ b/migrations/20260425183515_init.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; diff --git a/migrations/20260425183515_init.up.sql b/migrations/20260425183515_init.up.sql new file mode 100644 index 0000000..83b5410 --- /dev/null +++ b/migrations/20260425183515_init.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS users ( + uuid UUID PRIMARY KEY, + username VARCHAR(10) NOT NULL + CHECK (username ~ '^[a-z0-9]{1,10}$') UNIQUE, + email VARCHAR(255) NOT NULL + CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + password_hash TEXT NOT NULL, + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +}