Codevault initial commit
This commit is contained in:
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.git
|
||||||
|
codevault
|
||||||
|
codevault.exe
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
|
config*.yaml*
|
||||||
|
docker-compose.yaml
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/codevault
|
||||||
|
/codevault.exe
|
||||||
|
/*.db*
|
||||||
|
/*.log
|
||||||
|
config*.yaml*
|
||||||
|
docker-compose.yaml
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM golang:1.26-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -o /out/codevault .
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /out/codevault ./codevault
|
||||||
|
|
||||||
|
# /app/data's ownership is picked up by Docker as the initial content of any
|
||||||
|
# named volume mounted there, so the "nobody" user can still write the DB file.
|
||||||
|
RUN mkdir -p /app/data && chown -R nobody:nobody /app/data
|
||||||
|
USER nobody
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["./codevault"]
|
||||||
|
CMD ["run", "--config", "/app/config.yaml"]
|
||||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
build:
|
||||||
|
go build
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
GOOS=linux go build
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
docker build -t codevault .
|
||||||
|
|
||||||
|
# go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.5.1
|
||||||
|
generate:
|
||||||
|
go generate ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
config-example:
|
||||||
|
go run . config-example ./config.yaml
|
||||||
|
|
||||||
|
add-user:
|
||||||
|
go run . add-user --login admin
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run . run --config ./config.yaml
|
||||||
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# CodeVault
|
||||||
|
|
||||||
|
A small HTTP service that stores string values under access codes: write a
|
||||||
|
value with `setValue` under a code, later retrieve it with `getValue` using
|
||||||
|
that code. Basic Auth protects writes; reads are open but rate-limited by
|
||||||
|
failed-lookup count per client IP to slow down code guessing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- `POST /api/v1/setValue` — store a value under a code (Basic Auth required).
|
||||||
|
- `POST /api/v1/getValue` — retrieve a value by code (public, rate-limited).
|
||||||
|
- Interactive API docs at `/api` (Swagger UI, embedded assets — no CDN).
|
||||||
|
- Prometheus metrics for HTTP requests and business events (successful/failed
|
||||||
|
gets and sets).
|
||||||
|
- OpenTelemetry logging, with optional OTLP export.
|
||||||
|
- SQLite storage (pure Go driver, no CGO).
|
||||||
|
- Configurable trusted proxies so client IPs are resolved correctly behind a
|
||||||
|
reverse proxy (e.g. nginx).
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### Locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build
|
||||||
|
./codevault config-example ./config.yaml # write a starter config
|
||||||
|
./codevault add-user --db-path ./codevault.db --login admin # prompts for a password
|
||||||
|
./codevault run --config ./config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via the Makefile: `make build`, `make config-example`, `make add-user`,
|
||||||
|
`make run`.
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp docker-compose.yaml.example docker-compose.yaml
|
||||||
|
go run . config-example ./config.yaml # or hand-write one, see Configuration below
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose exec codevault ./codevault add-user --db-path /app/data/codevault.db --login admin
|
||||||
|
```
|
||||||
|
|
||||||
|
The compose file mounts `config.yaml` read-only and keeps the SQLite database
|
||||||
|
in a named volume (`codevault-data`). Edit `server.trusted_proxies` in your
|
||||||
|
`config.yaml` to match the proxy/network actually in front of the container
|
||||||
|
(gin needs IPs/CIDRs, not hostnames).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
`config-example` writes a documented starting point. Key sections:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment: development # development | production
|
||||||
|
server:
|
||||||
|
address: :8080
|
||||||
|
trusted_proxies: # IPs/CIDRs allowed to set X-Forwarded-For / X-Real-IP
|
||||||
|
- 172.16.0.0/12 # empty/omitted = trust nothing, use the raw remote address
|
||||||
|
database:
|
||||||
|
path: ./codevault.db
|
||||||
|
rate_limit:
|
||||||
|
max_failures: 10 # failed getValue lookups per IP before 404-ing everything
|
||||||
|
window: 1m0s # sliding window for max_failures
|
||||||
|
observability:
|
||||||
|
logging:
|
||||||
|
stderr: INFO # DEBUG|INFO|WARNING|ERROR|PANIC|FATAL, empty disables
|
||||||
|
files:
|
||||||
|
INFO: ./info.log # per-level log files, optional
|
||||||
|
exporter: # optional OTLP log export
|
||||||
|
level: INFO
|
||||||
|
endpoint: http://127.0.0.1:4317/v1/logs
|
||||||
|
transport: grpc # grpc | http
|
||||||
|
metrics:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 9092 # Prometheus /metrics endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Full OpenAPI spec: [internal/api/openapi.yaml](internal/api/openapi.yaml).
|
||||||
|
When the server is running, browse it interactively at `/api`, or fetch the
|
||||||
|
raw spec from `/api/openapi.json`.
|
||||||
|
|
||||||
|
- `POST /api/v1/getValue` `{"code": "..."}` → `200 {"value": "..."}` /
|
||||||
|
`404` (not found or rate-limited) / `400` (bad body).
|
||||||
|
- `POST /api/v1/setValue` `{"code": "...", "value": "..."}` (Basic Auth) →
|
||||||
|
`204` / `400` (bad body) / `401` (bad credentials).
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Exposed on `http://<metrics.host>:<metrics.port>/metrics`:
|
||||||
|
|
||||||
|
- `http_requests_total`, `http_request_duration_seconds` — generic per-route
|
||||||
|
HTTP metrics.
|
||||||
|
- `codevault_get_value_success_total{code}` — successful getValue lookups, by
|
||||||
|
code.
|
||||||
|
- `codevault_get_value_failed_total{reason}` — failed lookups, `reason` is
|
||||||
|
`not_found` or `rate_limited`.
|
||||||
|
- `codevault_set_value_success_total{user}` — successful setValue writes, by
|
||||||
|
authenticated login.
|
||||||
|
- `codevault_set_value_failed_total{reason}` — failed writes, `reason` is
|
||||||
|
`bad_request` or `unauthorized`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build # go build
|
||||||
|
make lint # golangci-lint
|
||||||
|
make generate # regenerate internal/api from openapi.yaml (oapi-codegen)
|
||||||
|
make test # go test ./...
|
||||||
|
make docker-build
|
||||||
|
```
|
||||||
83
cmd/add_user.go
Normal file
83
cmd/add_user.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/runcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var addUserCmd = &cobra.Command{
|
||||||
|
Use: "add-user",
|
||||||
|
Short: "Add or update a user in the database",
|
||||||
|
Long: `Add or update a user in the database. Does not read the application config -
|
||||||
|
only a database path is needed; if the database file does not exist it is
|
||||||
|
created together with its schema.`,
|
||||||
|
RunE: runAddUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addUserCmd.Flags().String("db-path", "./codevault.db", "Path to the SQLite database file")
|
||||||
|
addUserCmd.Flags().String("login", "", "Login of the user to add or update")
|
||||||
|
addUserCmd.Flags().String("password", "", "Password for the user (if omitted, prompted interactively; "+
|
||||||
|
"passing it as a flag can leak the password via shell history)")
|
||||||
|
_ = addUserCmd.MarkFlagRequired("login")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(addUserCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAddUser(cmd *cobra.Command, args []string) error {
|
||||||
|
dbPath, err := cmd.Flags().GetString("db-path")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on getting db-path option: %w", err)
|
||||||
|
}
|
||||||
|
login, err := cmd.Flags().GetString("login")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on getting login option: %w", err)
|
||||||
|
}
|
||||||
|
password, err := cmd.Flags().GetString("password")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on getting password option: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if password == "" {
|
||||||
|
password, err = readPasswordWithConfirmation(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on reading password: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runcmd.AddUser(dbPath, login, password); err != nil {
|
||||||
|
return fmt.Errorf("error on adding user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("user saved successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPasswordWithConfirmation(cmd *cobra.Command) (string, error) {
|
||||||
|
fd := int(os.Stdin.Fd())
|
||||||
|
|
||||||
|
cmd.Print("Password: ")
|
||||||
|
password, err := term.ReadPassword(fd)
|
||||||
|
cmd.Println()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error on reading password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Print("Confirm password: ")
|
||||||
|
confirmation, err := term.ReadPassword(fd)
|
||||||
|
cmd.Println()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error on reading password confirmation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(password) != string(confirmation) {
|
||||||
|
return "", fmt.Errorf("passwords do not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(password), nil
|
||||||
|
}
|
||||||
31
cmd/config_example.go
Normal file
31
cmd/config_example.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/runcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configExampleCmd = &cobra.Command{
|
||||||
|
Use: "config-example <config-path>",
|
||||||
|
Short: "Get server config example",
|
||||||
|
Long: `Generate and save to file a sample config for further manual editing.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
configPath := args[0]
|
||||||
|
|
||||||
|
err := runcmd.ConfigExample(configPath)
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
return fmt.Errorf("error on saving example config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(configExampleCmd)
|
||||||
|
}
|
||||||
23
cmd/root.go
Normal file
23
cmd/root.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "codevault",
|
||||||
|
Short: "CodeVault - store and retrieve secrets by access code",
|
||||||
|
Long: `CodeVault is a small HTTP service that stores string values under access codes.`,
|
||||||
|
SilenceUsage: true,
|
||||||
|
Version: internal.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
cmd/run.go
Normal file
123
cmd/run.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/config"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/logging"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||||
|
"github.com/kanootoko/codevault/internal/runcmd"
|
||||||
|
"github.com/kanootoko/codevault/internal/utils"
|
||||||
|
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Start the CodeVault server with the given config",
|
||||||
|
Long: `Start the CodeVault HTTP server using the given config file.`,
|
||||||
|
PreRunE: preRunE,
|
||||||
|
RunE: runE,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_ = runCmd.Flags().StringP("config", "c", "", "Path to config [CONFIG envvar]")
|
||||||
|
utils.TryFlagFromEnv(runCmd, "config", "CONFIG", true)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(runCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
ctx, cancelCtx = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
configPath, _ := cmd.Flags().GetString("config")
|
||||||
|
if configPath == "" {
|
||||||
|
return nil // will fail on RunE() with a proper error
|
||||||
|
}
|
||||||
|
|
||||||
|
appConfig, err := config.ReadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on reading config: %w", err)
|
||||||
|
}
|
||||||
|
if err = appConfig.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("error on config validation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, err = configureLoggerCtx(ctx, appConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to configure logger: %w", err)
|
||||||
|
}
|
||||||
|
logger := logging.LoggerMustFromContext(ctx)
|
||||||
|
ctx = gshutdown.CtxWithGracefulShutdown(ctx, logger.Named("graceful_shutdown"))
|
||||||
|
|
||||||
|
if err = configureObservability(ctx, appConfig.Observability); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure observability: %w", err)
|
||||||
|
}
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runE(cmd *cobra.Command, args []string) error {
|
||||||
|
configPath, err := cmd.Flags().GetString("config")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on getting config option: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appConfig, err := config.ReadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on reading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer postRun(cmd.Context())
|
||||||
|
|
||||||
|
err = runcmd.RunServer(cmd.Context(), appConfig)
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
return fmt.Errorf("error on running server: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func postRun(ctx context.Context) {
|
||||||
|
logger := logging.LoggerMustFromContext(ctx)
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
logger.Error("uncaught panic", zap.Any("panic data", err), zap.Stack("stack_trace"))
|
||||||
|
defer panic(err)
|
||||||
|
}
|
||||||
|
cancelCtx()
|
||||||
|
err := gshutdown.AwaitWithTimeout(ctx, time.Second*10)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("graceful shutdown failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
loggerCloseFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureLoggerCtx(ctx context.Context, appConfig config.Config) (context.Context, error) {
|
||||||
|
logger, closeFunc, err := logging.ConfigureLogger(ctx, appConfig.Observability.Logging, appConfig.Environment == config.ProductionEnvironment)
|
||||||
|
zap.ReplaceGlobals(logger)
|
||||||
|
loggerCloseFunc = closeFunc
|
||||||
|
if err != nil {
|
||||||
|
return ctx, fmt.Errorf("error on logging initialization: %w", err)
|
||||||
|
}
|
||||||
|
return logging.CtxWithLogger(ctx, logger), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureObservability(ctx context.Context, cfg config.ObservabilityConfig) error {
|
||||||
|
if cfg.Metrics.Host != "" {
|
||||||
|
if err := metrics.Configure(ctx, cfg.Metrics); err != nil {
|
||||||
|
return fmt.Errorf("error on starting Prometheus metrics handler: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var loggerCloseFunc func() // so the logger is closed after all other context-wide subtasks
|
||||||
|
var cancelCtx context.CancelFunc
|
||||||
30
docker-compose.yaml.example
Normal file
30
docker-compose.yaml.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
services:
|
||||||
|
codevault:
|
||||||
|
build: .
|
||||||
|
image: codevault:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
- "127.0.0.1:9092:9092"
|
||||||
|
volumes:
|
||||||
|
# config.yaml's database.path should point inside this volume, e.g. /app/data/codevault.db.
|
||||||
|
# server.trusted_proxies must list the nginx container/network's actual IP or CIDR
|
||||||
|
# (gin needs IPs/CIDRs, not hostnames) - e.g. this compose network's subnet.
|
||||||
|
- ./config.yaml:/app/config.yaml:ro
|
||||||
|
- codevault-data:/app/data
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 256M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 64M
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
codevault-data:
|
||||||
102
go.mod
Normal file
102
go.mod
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
module github.com/kanootoko/codevault
|
||||||
|
|
||||||
|
go 1.26.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/getkin/kin-openapi v0.146.0
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
github.com/oapi-codegen/runtime v1.7.0
|
||||||
|
github.com/prometheus/client_golang v1.24.1
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
github.com/swaggo/files v1.0.1
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelzap v0.20.0
|
||||||
|
go.opentelemetry.io/otel v1.45.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.67.0
|
||||||
|
go.opentelemetry.io/otel/log v0.21.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0
|
||||||
|
go.uber.org/multierr v1.11.0
|
||||||
|
go.uber.org/zap v1.28.0
|
||||||
|
golang.org/x/crypto v0.55.0
|
||||||
|
golang.org/x/term v0.45.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
modernc.org/sqlite v1.56.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/oasdiff/yaml v0.1.1 // indirect
|
||||||
|
github.com/oasdiff/yaml3 v0.0.14 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
|
github.com/prometheus/common v0.70.1 // indirect
|
||||||
|
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/swaggo/swag v1.8.12 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/net v0.57.0 // indirect
|
||||||
|
golang.org/x/sys v0.47.0 // indirect
|
||||||
|
golang.org/x/text v0.41.0 // indirect
|
||||||
|
golang.org/x/tools v0.48.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||||
|
google.golang.org/grpc v1.83.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
modernc.org/libc v1.74.4 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
324
go.sum
Normal file
324
go.sum
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
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/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
|
||||||
|
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
|
github.com/go-logr/logr v1.4.4/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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||||
|
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||||
|
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||||
|
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||||
|
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
|
||||||
|
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
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/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||||
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||||
|
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/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
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/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
|
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/oapi-codegen/runtime v1.7.0 h1:t7358VYPvNbWJ9gdAkIK/smVeHpBf6yp8VTsaZsb/7k=
|
||||||
|
github.com/oapi-codegen/runtime v1.7.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
|
||||||
|
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
||||||
|
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||||
|
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||||
|
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.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
|
||||||
|
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
|
||||||
|
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
|
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||||
|
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
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/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/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||||
|
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
|
||||||
|
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
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/otelzap v0.20.0 h1:wgsHT2HLf1KEZtCkd6ZGynPdeIyFsCSKsHyDBW9vEJk=
|
||||||
|
go.opentelemetry.io/contrib/bridges/otelzap v0.20.0/go.mod h1:NZCU/Hi3EdSS6LxcNJOO39Y5z95R9QY5iQ2Ym6IOFTU=
|
||||||
|
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||||
|
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0 h1:fvNHGyo3CdRv/DQveXqhqBxnKTDyRaC5sMSQxilX/A0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0/go.mod h1:zyGrjRKL2B/6+Jc/m4/otPoZqV2MY9ZjC/aBraRO7zc=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.67.0 h1:7IefDa35e6V3NoiqIeLDMDxMFyZDk5qcoC0Ax4cC16E=
|
||||||
|
go.opentelemetry.io/otel/exporters/prometheus v0.67.0/go.mod h1:nsPI1awTg5Vmg1YrommL2mVarVGlqc4yXOoKAkPRD0c=
|
||||||
|
go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA=
|
||||||
|
go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g=
|
||||||
|
go.opentelemetry.io/otel/log/logtest v0.21.0 h1:/Zr/0DoraAjiX91pZMn72uSDkd7hA+jn3CPU2y+2rWY=
|
||||||
|
go.opentelemetry.io/otel/log/logtest v0.21.0/go.mod h1:dyswW/l7aXiiCAmbKlt+Eg2NUnN6p1YrHQPBiV7QrLU=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||||
|
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||||
|
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0 h1:X+JBBgKlswCGYsmgL0CnoUUtlE//VB345c84jYAYkdQ=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.21.0/go.mod h1:HD1575K8e6sIFBBDd5tZB3t9DlMytWXq9FuR+Y4rfjE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||||
|
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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||||
|
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||||
|
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
|
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
|
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
|
||||||
|
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||||
|
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/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.0-20200615113413-eeeca48fe776/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=
|
||||||
|
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||||
|
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
|
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||||
|
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||||
|
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||||
|
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
|
||||||
|
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
4
internal/api/doc.go
Normal file
4
internal/api/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package api contains the OpenAPI-generated GIN strict-server code for CodeVault.
|
||||||
|
package api
|
||||||
|
|
||||||
|
//go:generate oapi-codegen --config=oapi-codegen.yaml openapi.yaml
|
||||||
363
internal/api/generated.go
Normal file
363
internal/api/generated.go
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
// Package api provides primitives to interact with the openapi HTTP API.
|
||||||
|
//
|
||||||
|
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
strictgin "github.com/oapi-codegen/runtime/strictmiddleware/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BasicAuthScopes = "basicAuth.Scopes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetValueRequest defines model for GetValueRequest.
|
||||||
|
type GetValueRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueResponse defines model for GetValueResponse.
|
||||||
|
type GetValueResponse struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValueRequest defines model for SetValueRequest.
|
||||||
|
type SetValueRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValueJSONRequestBody defines body for GetValue for application/json ContentType.
|
||||||
|
type GetValueJSONRequestBody = GetValueRequest
|
||||||
|
|
||||||
|
// SetValueJSONRequestBody defines body for SetValue for application/json ContentType.
|
||||||
|
type SetValueJSONRequestBody = SetValueRequest
|
||||||
|
|
||||||
|
// ServerInterface represents all server handlers.
|
||||||
|
type ServerInterface interface {
|
||||||
|
|
||||||
|
// (POST /api/v1/getValue)
|
||||||
|
GetValue(c *gin.Context)
|
||||||
|
|
||||||
|
// (POST /api/v1/setValue)
|
||||||
|
SetValue(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInterfaceWrapper converts contexts to parameters.
|
||||||
|
type ServerInterfaceWrapper struct {
|
||||||
|
Handler ServerInterface
|
||||||
|
HandlerMiddlewares []MiddlewareFunc
|
||||||
|
ErrorHandler func(*gin.Context, error, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiddlewareFunc func(c *gin.Context)
|
||||||
|
|
||||||
|
// GetValue operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) GetValue(c *gin.Context) {
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
middleware(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siw.Handler.GetValue(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) SetValue(c *gin.Context) {
|
||||||
|
|
||||||
|
c.Set(BasicAuthScopes, []string{})
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
middleware(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siw.Handler.SetValue(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinServerOptions provides options for the Gin server.
|
||||||
|
type GinServerOptions struct {
|
||||||
|
BaseURL string
|
||||||
|
Middlewares []MiddlewareFunc
|
||||||
|
ErrorHandler func(*gin.Context, error, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandlers creates http.Handler with routing matching OpenAPI spec.
|
||||||
|
func RegisterHandlers(router gin.IRouter, si ServerInterface) {
|
||||||
|
RegisterHandlersWithOptions(router, si, GinServerOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandlersWithOptions creates http.Handler with additional options
|
||||||
|
func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options GinServerOptions) {
|
||||||
|
errorHandler := options.ErrorHandler
|
||||||
|
if errorHandler == nil {
|
||||||
|
errorHandler = func(c *gin.Context, err error, statusCode int) {
|
||||||
|
c.JSON(statusCode, gin.H{"msg": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := ServerInterfaceWrapper{
|
||||||
|
Handler: si,
|
||||||
|
HandlerMiddlewares: options.Middlewares,
|
||||||
|
ErrorHandler: errorHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
router.POST(options.BaseURL+"/api/v1/getValue", wrapper.GetValue)
|
||||||
|
router.POST(options.BaseURL+"/api/v1/setValue", wrapper.SetValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValueRequestObject struct {
|
||||||
|
Body *GetValueJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValueResponseObject interface {
|
||||||
|
VisitGetValueResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValue200JSONResponse GetValueResponse
|
||||||
|
|
||||||
|
func (response GetValue200JSONResponse) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValue400Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetValue400Response) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetValue404Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response GetValue404Response) VisitGetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetValueRequestObject struct {
|
||||||
|
Body *SetValueJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetValueResponseObject interface {
|
||||||
|
VisitSetValueResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetValue204Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response SetValue204Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(204)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetValue400Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response SetValue400Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetValue401Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response SetValue401Response) VisitSetValueResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(401)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrictServerInterface represents all server handlers.
|
||||||
|
type StrictServerInterface interface {
|
||||||
|
|
||||||
|
// (POST /api/v1/getValue)
|
||||||
|
GetValue(ctx context.Context, request GetValueRequestObject) (GetValueResponseObject, error)
|
||||||
|
|
||||||
|
// (POST /api/v1/setValue)
|
||||||
|
SetValue(ctx context.Context, request SetValueRequestObject) (SetValueResponseObject, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StrictHandlerFunc = strictgin.StrictGinHandlerFunc
|
||||||
|
type StrictMiddlewareFunc = strictgin.StrictGinMiddlewareFunc
|
||||||
|
|
||||||
|
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
|
||||||
|
return &strictHandler{ssi: ssi, middlewares: middlewares}
|
||||||
|
}
|
||||||
|
|
||||||
|
type strictHandler struct {
|
||||||
|
ssi StrictServerInterface
|
||||||
|
middlewares []StrictMiddlewareFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue operation middleware
|
||||||
|
func (sh *strictHandler) GetValue(ctx *gin.Context) {
|
||||||
|
var request GetValueRequestObject
|
||||||
|
|
||||||
|
var body GetValueJSONRequestBody
|
||||||
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
|
ctx.Status(http.StatusBadRequest)
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.GetValue(ctx, request.(GetValueRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "GetValue")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(ctx, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
} else if validResponse, ok := response.(GetValueResponseObject); ok {
|
||||||
|
if err := validResponse.VisitGetValueResponse(ctx.Writer); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue operation middleware
|
||||||
|
func (sh *strictHandler) SetValue(ctx *gin.Context) {
|
||||||
|
var request SetValueRequestObject
|
||||||
|
|
||||||
|
var body SetValueJSONRequestBody
|
||||||
|
if err := ctx.ShouldBindJSON(&body); err != nil {
|
||||||
|
ctx.Status(http.StatusBadRequest)
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx *gin.Context, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.SetValue(ctx, request.(SetValueRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "SetValue")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(ctx, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
} else if validResponse, ok := response.(SetValueResponseObject); ok {
|
||||||
|
if err := validResponse.VisitSetValueResponse(ctx.Writer); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
|
"H4sIAAAAAAAC/7STwW7bMAyGX8XgdjRiZ+3Jt26HIYcBwwL0EuSgSEzCwhVVkQ4QFH73QZKTLcuKbgV6",
|
||||||
|
"siH95E9+pJ7B8mNgj14FumcQu8dHk3+/ot6bfsAf+DSgaDoKkQNGJcwCyw7TV48BoQPRSH4H41hDxKeB",
|
||||||
|
"IjroVkW1rk8q3jygVRjr3/JLYC94bXBI1687FNnfLJZvbKH+V+scX79YwViDoB0i6XGZwBbTjRGyd4Pu",
|
||||||
|
"z8BTTD6Fc4q9aoAxZSC/5VwJaZ9uvrDDezP0Wt19XyRzjELsoYP5rJ21qXoO6E0g6OBm1s5uoIZgdJ/N",
|
||||||
|
"GxOoOcyb3YQmM+HCJpExSuwXDrrzfKC0jKKf2R0LNa/oc4gJoSebg5oHSVWcdij9fYy4hQ4+NL+WrJk2",
|
||||||
|
"rPlzvcZLthoHzAdlOXLtn9r2Heyn7cv+DsVGClp4ZkG15cG7RPW22F9qFv5genLVhKjaJEZZfHstLgk9",
|
||||||
|
"6ylpNj2NRF4dyfJ9R7J8y0hebFOUU9j/g5tfi7+RCPldxbGiKc5GdOiVTC8XDw261cUTW63HdeL8MwAA",
|
||||||
|
"//98/iCR6gQAAA==",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
// or error if failed to decode
|
||||||
|
func decodeSpec() ([]byte, error) {
|
||||||
|
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
|
||||||
|
}
|
||||||
|
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err = buf.ReadFrom(zr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawSpec = decodeSpecCached()
|
||||||
|
|
||||||
|
// a naive cached of a decoded swagger spec
|
||||||
|
func decodeSpecCached() func() ([]byte, error) {
|
||||||
|
data, err := decodeSpec()
|
||||||
|
return func() ([]byte, error) {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
|
||||||
|
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
|
||||||
|
res := make(map[string]func() ([]byte, error))
|
||||||
|
if len(pathToFile) > 0 {
|
||||||
|
res[pathToFile] = rawSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||||
|
// in this file. The external references of Swagger specification are resolved.
|
||||||
|
// The logic of resolving external references is tightly connected to "import-mapping" feature.
|
||||||
|
// Externally referenced files must be embedded in the corresponding golang packages.
|
||||||
|
// Urls can be supported but this task was out of the scope.
|
||||||
|
func GetSwagger() (swagger *openapi3.T, err error) {
|
||||||
|
resolvePath := PathToRawSpec("")
|
||||||
|
|
||||||
|
loader := openapi3.NewLoader()
|
||||||
|
loader.IsExternalRefsAllowed = true
|
||||||
|
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
|
||||||
|
pathToFile := url.String()
|
||||||
|
pathToFile = path.Clean(pathToFile)
|
||||||
|
getSpec, ok := resolvePath[pathToFile]
|
||||||
|
if !ok {
|
||||||
|
err1 := fmt.Errorf("path not found: %s", pathToFile)
|
||||||
|
return nil, err1
|
||||||
|
}
|
||||||
|
return getSpec()
|
||||||
|
}
|
||||||
|
var specData []byte
|
||||||
|
specData, err = rawSpec()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
swagger, err = loader.LoadFromData(specData)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
55
internal/api/handlers.go
Normal file
55
internal/api/handlers.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/db"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StrictHandler implements StrictServerInterface.
|
||||||
|
type StrictHandler struct {
|
||||||
|
Secrets *db.SecretsRepository
|
||||||
|
Metrics metrics.ValueMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(secrets *db.SecretsRepository, m metrics.ValueMetrics) *StrictHandler {
|
||||||
|
return &StrictHandler{Secrets: secrets, Metrics: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StrictHandler) GetValue(ctx context.Context, request GetValueRequestObject) (GetValueResponseObject, error) {
|
||||||
|
if request.Body == nil || request.Body.Code == "" {
|
||||||
|
return GetValue400Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := h.Secrets.Get(ctx, request.Body.Code)
|
||||||
|
if errors.Is(err, db.ErrNotFound) {
|
||||||
|
h.Metrics.GetFailedTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", "not_found")))
|
||||||
|
return GetValue404Response{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Metrics.GetSuccessTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("code", request.Body.Code)))
|
||||||
|
return GetValue200JSONResponse{Value: value}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StrictHandler) SetValue(ctx context.Context, request SetValueRequestObject) (SetValueResponseObject, error) {
|
||||||
|
if request.Body == nil || request.Body.Code == "" || request.Body.Value == "" || len(request.Body.Value) > 10*1024*1024 {
|
||||||
|
h.Metrics.SetFailedTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", "bad_request")))
|
||||||
|
return SetValue400Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Secrets.Set(ctx, request.Body.Code, request.Body.Value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
login, _ := LoginFromContext(ctx)
|
||||||
|
h.Metrics.SetSuccessTotal.Add(ctx, 1, metric.WithAttributes(attribute.String("user", login)))
|
||||||
|
return SetValue204Response{}, nil
|
||||||
|
}
|
||||||
81
internal/api/middleware.go
Normal file
81
internal/api/middleware.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/db"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||||
|
"github.com/kanootoko/codevault/internal/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const loginContextKey contextKey = "login"
|
||||||
|
|
||||||
|
// LoginFromContext returns the Basic Auth login stashed by BasicAuthMiddleware,
|
||||||
|
// if any. Strict-server handlers only see context.Context, not *gin.Context, so
|
||||||
|
// this is how they recover the authenticated login.
|
||||||
|
func LoginFromContext(ctx context.Context) (string, bool) {
|
||||||
|
login, ok := ctx.Value(loginContextKey).(string)
|
||||||
|
return login, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicAuthMiddleware checks the request's HTTP Basic Auth credentials against
|
||||||
|
// the DB-backed user store.
|
||||||
|
func BasicAuthMiddleware(users *db.UsersRepository, vm metrics.ValueMetrics) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
login, password, ok := c.Request.BasicAuth()
|
||||||
|
if !ok || users.Verify(c.Request.Context(), login, password) != nil {
|
||||||
|
vm.SetFailedTotal.Add(c.Request.Context(), 1, metric.WithAttributes(attribute.String("reason", "unauthorized")))
|
||||||
|
c.Header("WWW-Authenticate", `Basic realm="codevault"`)
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), loginContextKey, login))
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitMiddleware short-circuits to 404 once the client IP has accumulated
|
||||||
|
// too many failures, without touching the handler (and thus the database). A
|
||||||
|
// short-circuited request counts as a failure itself, same as a genuine 404 from
|
||||||
|
// the handler.
|
||||||
|
func RateLimitMiddleware(limiter *ratelimit.Limiter, vm metrics.ValueMetrics) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ip := c.ClientIP()
|
||||||
|
if !limiter.Allowed(ip) {
|
||||||
|
limiter.RecordFailure(ip)
|
||||||
|
vm.GetFailedTotal.Add(c.Request.Context(), 1, metric.WithAttributes(attribute.String("reason", "rate_limited")))
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
if c.Writer.Status() == http.StatusNotFound {
|
||||||
|
limiter.RecordFailure(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstrumentMiddleware records request count and duration metrics per route.
|
||||||
|
func InstrumentMiddleware(route string, m metrics.HTTPMetrics) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
c.Next()
|
||||||
|
duration := time.Since(start).Seconds()
|
||||||
|
|
||||||
|
attrs := metric.WithAttributes(
|
||||||
|
attribute.String("route", route),
|
||||||
|
attribute.Int("status", c.Writer.Status()),
|
||||||
|
)
|
||||||
|
m.RequestsTotal.Add(c.Request.Context(), 1, attrs)
|
||||||
|
m.RequestDuration.Record(c.Request.Context(), duration, metric.WithAttributes(attribute.String("route", route)))
|
||||||
|
}
|
||||||
|
}
|
||||||
7
internal/api/oapi-codegen.yaml
Normal file
7
internal/api/oapi-codegen.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package: api
|
||||||
|
generate:
|
||||||
|
gin-server: true
|
||||||
|
strict-server: true
|
||||||
|
models: true
|
||||||
|
embedded-spec: true
|
||||||
|
output: generated.go
|
||||||
73
internal/api/openapi.yaml
Normal file
73
internal/api/openapi.yaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: CodeVault API
|
||||||
|
version: "1.0.0"
|
||||||
|
paths:
|
||||||
|
/api/v1/getValue:
|
||||||
|
post:
|
||||||
|
operationId: getValue
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GetValueRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Value found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GetValueResponse'
|
||||||
|
'400':
|
||||||
|
description: Invalid request body
|
||||||
|
'404':
|
||||||
|
description: Value not found
|
||||||
|
/api/v1/setValue:
|
||||||
|
post:
|
||||||
|
operationId: setValue
|
||||||
|
security:
|
||||||
|
- basicAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SetValueRequest'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: Value stored
|
||||||
|
'400':
|
||||||
|
description: Invalid request body
|
||||||
|
'401':
|
||||||
|
description: Missing or invalid credentials
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
basicAuth:
|
||||||
|
type: http
|
||||||
|
scheme: basic
|
||||||
|
schemas:
|
||||||
|
GetValueRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
GetValueResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- value
|
||||||
|
properties:
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
SetValueRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- value
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
35
internal/api/swagger.go
Normal file
35
internal/api/swagger.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterSwaggerHandlers serves the OpenAPI spec and an interactive Swagger UI
|
||||||
|
// (embedded swagger-ui-dist assets, no CDN) under /api.
|
||||||
|
func RegisterSwaggerHandlers(router *gin.Engine) error {
|
||||||
|
spec, err := GetSwagger()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on building swagger specification: %w", err)
|
||||||
|
}
|
||||||
|
specBody, err := spec.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on marshaling swagger specification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/docs/*any (rather than /api/*any) so this wildcard doesn't collide with
|
||||||
|
// the static /api/openapi.json route at the same tree level.
|
||||||
|
router.GET("/api/openapi.json", func(c *gin.Context) {
|
||||||
|
c.Data(http.StatusOK, "application/json", specBody)
|
||||||
|
})
|
||||||
|
router.GET("/api", func(c *gin.Context) {
|
||||||
|
c.Redirect(http.StatusFound, "/api/docs/index.html")
|
||||||
|
})
|
||||||
|
router.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL("/api/openapi.json")))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
37
internal/auth/password.go
Normal file
37
internal/auth/password.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// saltedInput mixes the login into the password material before bcrypt hashing, using
|
||||||
|
// the login as a per-user salt: a leaked hash can't be replayed against a different
|
||||||
|
// login, and pre-hashing keeps the input short regardless of login/password length
|
||||||
|
// (bcrypt only considers the first 72 bytes of its input).
|
||||||
|
func saltedInput(login, password string) []byte {
|
||||||
|
sum := sha256.Sum256([]byte(login + "\x00" + password))
|
||||||
|
return []byte(base64.StdEncoding.EncodeToString(sum[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dummyHash is compared against on unknown logins so that a failed lookup takes
|
||||||
|
// roughly the same time as a real password comparison, avoiding user enumeration.
|
||||||
|
var dummyHash, _ = bcrypt.GenerateFromPassword(saltedInput("dummy-login", "dummy-password"), bcrypt.DefaultCost)
|
||||||
|
|
||||||
|
func Hash(login, password string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword(saltedInput(login, password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Compare(login, hash, password string) bool {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hash), saltedInput(login, password)) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompareDummy() {
|
||||||
|
_ = bcrypt.CompareHashAndPassword(dummyHash, saltedInput("dummy-login", "dummy-password"))
|
||||||
|
}
|
||||||
74
internal/config/config.go
Normal file
74
internal/config/config.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrLoggingDisabled error = errors.New("logging is disabled")
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Environment Environment `yaml:"environment"`
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||||
|
Observability ObservabilityConfig `yaml:"observability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = addValidationError(err, "environment validation error", c.Environment.PreprocessAndValidate())
|
||||||
|
err = addValidationError(err, "server section validation error", c.Server.Validate())
|
||||||
|
err = addValidationError(err, "database section validation error", c.Database.Validate())
|
||||||
|
err = addValidationError(err, "rate_limit section validation error", c.RateLimit.Validate())
|
||||||
|
err = addValidationError(err, "observability section validation error", c.Observability.Validate())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(path string) (Config, error) {
|
||||||
|
var c Config
|
||||||
|
yamlFile, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("could not read config: %w", err)
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(yamlFile, &c)
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("could not parse yaml: %w", err)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Environment string
|
||||||
|
|
||||||
|
var (
|
||||||
|
ProductionEnvironment Environment = "production"
|
||||||
|
DevelopmentEnvironment Environment = "development"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Environment) PreprocessAndValidate() error {
|
||||||
|
preprocessed := strings.ToLower(string(*e))
|
||||||
|
switch preprocessed {
|
||||||
|
case string(ProductionEnvironment), "prod":
|
||||||
|
*e = ProductionEnvironment
|
||||||
|
return nil
|
||||||
|
case string(DevelopmentEnvironment), "dev":
|
||||||
|
*e = DevelopmentEnvironment
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid Environment value: '%v'", *e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addValidationError(err error, text string, validationError error) error {
|
||||||
|
if validationError == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return multierr.Append(err, fmt.Errorf("%s: %w", text, validationError))
|
||||||
|
}
|
||||||
14
internal/config/database.go
Normal file
14
internal/config/database.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *DatabaseConfig) Validate() error {
|
||||||
|
if dc.Path == "" {
|
||||||
|
return fmt.Errorf("path is missing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
131
internal/config/observability.go
Normal file
131
internal/config/observability.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ObservabilityConfig struct {
|
||||||
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
Metrics MetricsConfig `yaml:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oc *ObservabilityConfig) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = addValidationError(err, "logging section validation error", oc.Logging.Validate())
|
||||||
|
err = addValidationError(err, "metrics section validation error", oc.Metrics.Validate())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingConfig struct {
|
||||||
|
Stderr LoggingLevel `yaml:"stderr"`
|
||||||
|
Files map[LoggingLevel]string `yaml:"files"`
|
||||||
|
Exporter LogsExporterConfig `yaml:"exporter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LoggingConfig) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, loggingErr := lc.Stderr.ToZapLevel()
|
||||||
|
if !errors.Is(loggingErr, ErrLoggingDisabled) {
|
||||||
|
err = addValidationError(err, "stderr logging level validation error", loggingErr)
|
||||||
|
}
|
||||||
|
for level, filename := range lc.Files {
|
||||||
|
_, loggingErr = level.ToZapLevel()
|
||||||
|
err = addValidationError(err, fmt.Sprintf("logging level validation error for file '%s'", filename), loggingErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogsExporterConfig struct {
|
||||||
|
Level LoggingLevel `yaml:"level"`
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
Transport OTLPTransportType `yaml:"transport"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lec *LogsExporterConfig) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, loggingErr := lec.Level.ToZapLevel()
|
||||||
|
if errors.Is(loggingErr, ErrLoggingDisabled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = addValidationError(err, "logging level validation error", loggingErr)
|
||||||
|
if !strings.HasPrefix(lec.Endpoint, "http") {
|
||||||
|
return fmt.Errorf("endpoint must has http(s) prefix")
|
||||||
|
}
|
||||||
|
err = addValidationError(err, "transport type validation error", lec.Transport.PreprocessAndValidate())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type OTLPTransportType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
GRPCTransportType OTLPTransportType = "grpc"
|
||||||
|
HTTPTransportType OTLPTransportType = "http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (otp *OTLPTransportType) PreprocessAndValidate() error {
|
||||||
|
if *otp == "" {
|
||||||
|
*otp = GRPCTransportType
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
preprocessed := OTLPTransportType(strings.ToLower(string(*otp)))
|
||||||
|
switch preprocessed {
|
||||||
|
case GRPCTransportType, HTTPTransportType:
|
||||||
|
*otp = preprocessed
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid OTLPTransportType: '%v'", *otp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsConfig struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mc *MetricsConfig) Validate() error {
|
||||||
|
if mc.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if mc.Port < 1 || mc.Port > 65535 {
|
||||||
|
return fmt.Errorf("port value is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingLevel string
|
||||||
|
|
||||||
|
var (
|
||||||
|
DebugLoggingLevel LoggingLevel = "DEBUG"
|
||||||
|
InfoLoggingLevel LoggingLevel = "INFO"
|
||||||
|
WarningLoggingLevel LoggingLevel = "WARNING"
|
||||||
|
ErrorLoggingLevel LoggingLevel = "ERROR"
|
||||||
|
PanicLoggingLevel LoggingLevel = "PANIC"
|
||||||
|
FatalLoggingLevel LoggingLevel = "FATAL"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l LoggingLevel) ToZapLevel() (zapcore.Level, error) {
|
||||||
|
if l == "" {
|
||||||
|
return zapcore.InvalidLevel, ErrLoggingDisabled
|
||||||
|
}
|
||||||
|
levelString := strings.ToLower(string(l))
|
||||||
|
mapping := map[string]zapcore.Level{
|
||||||
|
"debug": zapcore.DebugLevel,
|
||||||
|
"info": zapcore.InfoLevel,
|
||||||
|
"warn": zapcore.WarnLevel,
|
||||||
|
"warning": zapcore.WarnLevel,
|
||||||
|
"error": zapcore.ErrorLevel,
|
||||||
|
"panic": zapcore.PanicLevel,
|
||||||
|
"fatal": zapcore.FatalLevel,
|
||||||
|
}
|
||||||
|
if level, ok := mapping[levelString]; ok {
|
||||||
|
return level, nil
|
||||||
|
}
|
||||||
|
return zapcore.InvalidLevel, fmt.Errorf("invalid logging level value: %v", l)
|
||||||
|
}
|
||||||
21
internal/config/ratelimit.go
Normal file
21
internal/config/ratelimit.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
MaxFailures int `yaml:"max_failures"`
|
||||||
|
Window time.Duration `yaml:"window"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RateLimitConfig) Validate() error {
|
||||||
|
if rc.MaxFailures <= 0 {
|
||||||
|
return fmt.Errorf("max_failures must be positive")
|
||||||
|
}
|
||||||
|
if rc.Window <= 0 {
|
||||||
|
return fmt.Errorf("window must be positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
37
internal/config/server.go
Normal file
37
internal/config/server.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerConfig struct {
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
|
||||||
|
// TrustedProxies lists IPs/CIDRs (e.g. of a fronting nginx) allowed to set
|
||||||
|
// X-Forwarded-For/X-Real-IP when gin resolves the client IP. Empty means no
|
||||||
|
// forwarded header is trusted, and the raw remote address is used instead.
|
||||||
|
TrustedProxies []string `yaml:"trusted_proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *ServerConfig) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if sc.Address == "" {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("address is missing"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proxy := range sc.TrustedProxies {
|
||||||
|
if _, parseErr := netip.ParsePrefix(proxy); parseErr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, parseErr := netip.ParseAddr(proxy); parseErr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = multierr.Append(err, fmt.Errorf("invalid trusted_proxies entry %q: must be an IP or CIDR", proxy))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
30
internal/db/db.go
Normal file
30
internal/db/db.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed schema.sql
|
||||||
|
var schema string
|
||||||
|
|
||||||
|
// Open opens (creating the file if it does not exist) the SQLite database at path
|
||||||
|
// and applies the embedded schema.
|
||||||
|
func Open(path string) (*sql.DB, error) {
|
||||||
|
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)", path)
|
||||||
|
database, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open database: %w", err)
|
||||||
|
}
|
||||||
|
database.SetMaxOpenConns(1)
|
||||||
|
|
||||||
|
if _, err := database.Exec(schema); err != nil {
|
||||||
|
_ = database.Close()
|
||||||
|
return nil, fmt.Errorf("could not apply schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
12
internal/db/schema.sql
Normal file
12
internal/db/schema.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
login TEXT PRIMARY KEY,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS secrets (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
42
internal/db/secrets.go
Normal file
42
internal/db/secrets.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("secret not found")
|
||||||
|
|
||||||
|
type SecretsRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecretsRepository(db *sql.DB) *SecretsRepository {
|
||||||
|
return &SecretsRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretsRepository) Get(ctx context.Context, code string) (string, error) {
|
||||||
|
var value string
|
||||||
|
err := r.db.QueryRowContext(ctx, "SELECT value FROM secrets WHERE code = ?", code).Scan(&value)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not get secret: %w", err)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SecretsRepository) Set(ctx context.Context, code, value string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO secrets (code, value, updated_at)
|
||||||
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(code) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||||
|
`, code, value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not set secret: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
51
internal/db/users.go
Normal file
51
internal/db/users.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidCredentials = errors.New("invalid login or password")
|
||||||
|
|
||||||
|
type UsersRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsersRepository(db *sql.DB) *UsersRepository {
|
||||||
|
return &UsersRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UsersRepository) Upsert(ctx context.Context, login, passwordHash string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT INTO users (login, password_hash)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(login) DO UPDATE SET password_hash = excluded.password_hash
|
||||||
|
`, login, passwordHash)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not upsert user: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks the given login/password pair against the stored hash. A dummy hash
|
||||||
|
// comparison is performed for unknown logins so that lookup timing does not reveal
|
||||||
|
// whether the login exists.
|
||||||
|
func (r *UsersRepository) Verify(ctx context.Context, login, password string) error {
|
||||||
|
var passwordHash string
|
||||||
|
err := r.db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE login = ?", login).Scan(&passwordHash)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
auth.CompareDummy()
|
||||||
|
return ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get user: %w", err)
|
||||||
|
}
|
||||||
|
if !auth.Compare(login, passwordHash, password) {
|
||||||
|
return ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
116
internal/httpserver/server.go
Normal file
116
internal/httpserver/server.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Package httpserver assembles the gin engine (API routes + web UI) and runs it
|
||||||
|
// with graceful shutdown.
|
||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/api"
|
||||||
|
"github.com/kanootoko/codevault/internal/db"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/logging"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||||
|
"github.com/kanootoko/codevault/internal/ratelimit"
|
||||||
|
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||||
|
"github.com/kanootoko/codevault/internal/webui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const shutdownTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
const cleanupInterval = time.Minute
|
||||||
|
|
||||||
|
func New(
|
||||||
|
secrets *db.SecretsRepository,
|
||||||
|
users *db.UsersRepository,
|
||||||
|
limiter *ratelimit.Limiter,
|
||||||
|
m metrics.Metrics,
|
||||||
|
trustedProxies []string,
|
||||||
|
) (*gin.Engine, error) {
|
||||||
|
router := gin.New()
|
||||||
|
// Strict-server handlers only see context.Context (via *gin.Context, which
|
||||||
|
// implements it), not *gin.Context directly. Without this flag, gin.Context.Value
|
||||||
|
// never falls back to c.Request.Context(), so values stashed there by middleware
|
||||||
|
// (e.g. LoginFromContext) would be invisible to handlers.
|
||||||
|
router.ContextWithFallback = true
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
if err := router.SetTrustedProxies(trustedProxies); err != nil {
|
||||||
|
return nil, fmt.Errorf("error on setting trusted proxies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.GET("/", func(c *gin.Context) { serveStatic(c, webui.GetPagePath) })
|
||||||
|
router.GET("/set", func(c *gin.Context) { serveStatic(c, webui.SetPagePath) })
|
||||||
|
|
||||||
|
if err := api.RegisterSwaggerHandlers(router); err != nil {
|
||||||
|
return nil, fmt.Errorf("error on registering swagger handlers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := api.NewHandler(secrets, m.Value)
|
||||||
|
strictHandler := api.NewStrictHandler(handler, nil)
|
||||||
|
wrapper := api.ServerInterfaceWrapper{Handler: strictHandler}
|
||||||
|
|
||||||
|
router.POST("/api/v1/getValue",
|
||||||
|
api.InstrumentMiddleware("getValue", m.HTTP),
|
||||||
|
api.RateLimitMiddleware(limiter, m.Value),
|
||||||
|
wrapper.GetValue,
|
||||||
|
)
|
||||||
|
router.POST("/api/v1/setValue",
|
||||||
|
api.InstrumentMiddleware("setValue", m.HTTP),
|
||||||
|
api.BasicAuthMiddleware(users, m.Value),
|
||||||
|
wrapper.SetValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveStatic(c *gin.Context, path string) {
|
||||||
|
data, err := webui.Static.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the HTTP server and the rate limiter's cleanup goroutine, both
|
||||||
|
// tracked as gshutdown subtasks, and blocks until ctx is done.
|
||||||
|
func Run(ctx context.Context, address string, router *gin.Engine, limiter *ratelimit.Limiter) error {
|
||||||
|
logger := logging.LoggerMustFromContext(ctx)
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = gshutdown.SubtaskStarted(ctx, "http-server")
|
||||||
|
go func() {
|
||||||
|
defer func() { _ = gshutdown.SubtaskFinished(ctx, "http-server") }()
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
logger.Error("error on http server shutdown", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cleanupDone := make(chan struct{})
|
||||||
|
_ = gshutdown.SubtaskStarted(ctx, "ratelimit-cleanup")
|
||||||
|
go func() {
|
||||||
|
defer func() { _ = gshutdown.SubtaskFinished(ctx, "ratelimit-cleanup") }()
|
||||||
|
limiter.Run(cleanupDone, cleanupInterval)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
close(cleanupDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Info("starting http server", zap.String("address", address))
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
return fmt.Errorf("http server error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
139
internal/observability/logging/configure.go
Normal file
139
internal/observability/logging/configure.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/contrib/bridges/otelzap"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||||
|
"go.opentelemetry.io/otel/log/global"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal"
|
||||||
|
"github.com/kanootoko/codevault/internal/config"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigureLogger(ctx context.Context, cfg config.LoggingConfig, isProductionEnv bool) (logger *zap.Logger, closeFunc func(), err error) {
|
||||||
|
stderrLevel, err := cfg.Stderr.ToZapLevel()
|
||||||
|
if err != nil && !errors.Is(err, config.ErrLoggingDisabled) {
|
||||||
|
return nil, nil, fmt.Errorf("error on getting stderr logging level: %w", err)
|
||||||
|
}
|
||||||
|
fileLoggers := make(map[string]zapcore.Level)
|
||||||
|
for level, file := range cfg.Files {
|
||||||
|
l, err := level.ToZapLevel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error on getting file logging level: %w", err)
|
||||||
|
}
|
||||||
|
fileLoggers[file] = l
|
||||||
|
}
|
||||||
|
var stderrEncoder zapcore.Encoder
|
||||||
|
var fileEncoder zapcore.Encoder
|
||||||
|
{
|
||||||
|
var fileLoggerCfg zapcore.EncoderConfig
|
||||||
|
var stderrLoggerCfg zapcore.EncoderConfig
|
||||||
|
if isProductionEnv {
|
||||||
|
fileLoggerCfg = zap.NewProductionEncoderConfig()
|
||||||
|
stderrLoggerCfg = zap.NewProductionEncoderConfig()
|
||||||
|
} else {
|
||||||
|
fileLoggerCfg = zap.NewDevelopmentEncoderConfig()
|
||||||
|
stderrLoggerCfg = zap.NewDevelopmentEncoderConfig()
|
||||||
|
}
|
||||||
|
fileLoggerCfg.TimeKey = "timestamp"
|
||||||
|
fileLoggerCfg.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||||
|
|
||||||
|
stderrLoggerCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
|
|
||||||
|
stderrEncoder = zapcore.NewConsoleEncoder(stderrLoggerCfg)
|
||||||
|
fileEncoder = zapcore.NewJSONEncoder(fileLoggerCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHandlers := make([]*os.File, 0, len(fileLoggers))
|
||||||
|
zapWriters := make([]zapcore.Core, 0, len(fileLoggers)+1)
|
||||||
|
for filename, level := range fileLoggers {
|
||||||
|
fileHandler, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
for _, file := range fileHandlers[:len(fileHandlers)-1] {
|
||||||
|
_ = file.Close()
|
||||||
|
}
|
||||||
|
return nil, nil, fmt.Errorf("error on log file creation: %w", err)
|
||||||
|
}
|
||||||
|
fileHandlers = append(fileHandlers, fileHandler)
|
||||||
|
fileSync := zapcore.AddSync(fileHandler)
|
||||||
|
zapWriters = append(zapWriters, zapcore.NewCore(fileEncoder, fileSync, zap.NewAtomicLevelAt(level)))
|
||||||
|
}
|
||||||
|
|
||||||
|
stderrSync := zapcore.AddSync(os.Stderr)
|
||||||
|
if stderrLevel != zapcore.InvalidLevel {
|
||||||
|
zapWriters = append(zapWriters, zapcore.NewCore(stderrEncoder, stderrSync, zap.NewAtomicLevelAt(stderrLevel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var otlpCloseFunc func()
|
||||||
|
if cfg.Exporter.Level != "" {
|
||||||
|
var otlpWriter zapcore.Core
|
||||||
|
var err error
|
||||||
|
otlpWriter, otlpCloseFunc, err = configureOtelLogging(ctx, cfg.Exporter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot configure OpenTelemetry log exporter: %w", err)
|
||||||
|
}
|
||||||
|
zapWriters = append(zapWriters, otlpWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
core := zapcore.NewTee(
|
||||||
|
zapWriters...,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = zap.New(core)
|
||||||
|
|
||||||
|
closeFunc = func() {
|
||||||
|
_ = logger.Sync()
|
||||||
|
for _, file := range fileHandlers {
|
||||||
|
_ = file.Close()
|
||||||
|
}
|
||||||
|
if otlpCloseFunc != nil {
|
||||||
|
otlpCloseFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("configured logging", zap.Any("config", cfg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureOtelLogging(ctx context.Context, cfg config.LogsExporterConfig) (zapcore.Core, func(), error) {
|
||||||
|
var exporter log.Exporter
|
||||||
|
var err error
|
||||||
|
if cfg.Transport == config.HTTPTransportType {
|
||||||
|
exporter, err = otlploghttp.New(ctx, otlploghttp.WithEndpointURL(cfg.Endpoint))
|
||||||
|
} else {
|
||||||
|
exporter, err = otlploggrpc.New(ctx, otlploggrpc.WithEndpointURL(cfg.Endpoint))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot create OpenTelemetry logs exporter: %w", err)
|
||||||
|
}
|
||||||
|
res, err := observability.GetOtelResource()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot get OpenTelemetry resource: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := log.NewLoggerProvider(
|
||||||
|
log.WithProcessor(log.NewBatchProcessor(exporter)),
|
||||||
|
log.WithResource(res),
|
||||||
|
)
|
||||||
|
|
||||||
|
closeFunc := func() {
|
||||||
|
shutdownContext, cancel := context.WithTimeout(context.Background(), time.Millisecond*300)
|
||||||
|
defer cancel()
|
||||||
|
_ = provider.ForceFlush(shutdownContext)
|
||||||
|
_ = provider.Shutdown(shutdownContext)
|
||||||
|
<-shutdownContext.Done()
|
||||||
|
}
|
||||||
|
global.SetLoggerProvider(provider)
|
||||||
|
|
||||||
|
return otelzap.NewCore(internal.PackageName, otelzap.WithLoggerProvider(provider)), closeFunc, nil
|
||||||
|
}
|
||||||
41
internal/observability/logging/ctx.go
Normal file
41
internal/observability/logging/ctx.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type loggerKeyType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
loggerKey loggerKeyType = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotInitialized = errors.New("logger has not been initialized in the given context")
|
||||||
|
|
||||||
|
func CtxWithLogger(ctx context.Context, logger *zap.Logger) context.Context {
|
||||||
|
if loggerCtx, ok := ctx.Value(loggerKey).(*zap.Logger); ok {
|
||||||
|
if loggerCtx == logger {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, loggerKey, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggerFromCtx(ctx context.Context) (*zap.Logger, error) {
|
||||||
|
logger := ctx.Value(loggerKey)
|
||||||
|
if logger == nil {
|
||||||
|
return nil, ErrNotInitialized
|
||||||
|
}
|
||||||
|
return logger.(*zap.Logger), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggerMustFromContext(ctx context.Context) *zap.Logger {
|
||||||
|
logger, err := LoggerFromCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
83
internal/observability/metrics/configure.go
Normal file
83
internal/observability/metrics/configure.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||||
|
otelmetric "go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal"
|
||||||
|
"github.com/kanootoko/codevault/internal/config"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability"
|
||||||
|
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure starts a Prometheus server which serves metrics and finishes on context finish
|
||||||
|
func Configure(ctx context.Context, cfg config.MetricsConfig) error {
|
||||||
|
if cfg.Host == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res, err := observability.GetOtelResource()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get OpenTelemetry resource: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
meterProvider, err := newMeterProvider(res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot instanciate OpenTelemetry meter provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
otel.SetMeterProvider(meterProvider)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
server := http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: time.Second * 20,
|
||||||
|
ReadHeaderTimeout: time.Second * 10,
|
||||||
|
WriteTimeout: time.Second * 20,
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on creating listener for prometheus metrics: %w", err)
|
||||||
|
}
|
||||||
|
go func() { _ = server.Serve(listener) }()
|
||||||
|
_ = gshutdown.SubtaskStarted(ctx, "prometheus")
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
_ = listener.Close()
|
||||||
|
_ = gshutdown.SubtaskFinished(ctx, "prometheus")
|
||||||
|
}()
|
||||||
|
<-ctx.Done()
|
||||||
|
_ = server.Shutdown(context.Background())
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMeter() otelmetric.Meter {
|
||||||
|
mp := otel.GetMeterProvider()
|
||||||
|
return mp.Meter(internal.PackageName, otelmetric.WithInstrumentationVersion(internal.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) {
|
||||||
|
metricExporter, err := prometheus.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meterProvider := metric.NewMeterProvider(
|
||||||
|
metric.WithResource(res),
|
||||||
|
metric.WithReader(metricExporter),
|
||||||
|
)
|
||||||
|
return meterProvider, nil
|
||||||
|
}
|
||||||
110
internal/observability/metrics/metrics.go
Normal file
110
internal/observability/metrics/metrics.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
HTTP HTTPMetrics
|
||||||
|
Value ValueMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMetrics(meter metric.Meter) Metrics {
|
||||||
|
metrics := Metrics{
|
||||||
|
HTTP: newHTTPMetrics(meter),
|
||||||
|
Value: newValueMetrics(meter),
|
||||||
|
}
|
||||||
|
addObservableMetrics(meter)
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPMetrics struct {
|
||||||
|
// Total HTTP requests with [route, status]
|
||||||
|
RequestsTotal metric.Int64Counter
|
||||||
|
|
||||||
|
// Request duration histogram in seconds with [route]
|
||||||
|
RequestDuration metric.Float64Histogram
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPMetrics(meter metric.Meter) HTTPMetrics {
|
||||||
|
requestsTotal, _ := meter.Int64Counter(
|
||||||
|
"http_requests_total",
|
||||||
|
metric.WithDescription("Total number of HTTP requests"),
|
||||||
|
)
|
||||||
|
requestDuration, _ := meter.Float64Histogram(
|
||||||
|
"http_request_duration_seconds",
|
||||||
|
metric.WithDescription("HTTP request duration histogram in seconds"),
|
||||||
|
metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),
|
||||||
|
metric.WithUnit("s"),
|
||||||
|
)
|
||||||
|
return HTTPMetrics{
|
||||||
|
RequestsTotal: requestsTotal,
|
||||||
|
RequestDuration: requestDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueMetrics struct {
|
||||||
|
// Successful getValue lookups, with [code]. Only successful lookups carry a
|
||||||
|
// code label since the set of stored codes is bounded, unlike guessed/failed ones.
|
||||||
|
GetSuccessTotal metric.Int64Counter
|
||||||
|
|
||||||
|
// Failed getValue lookups, with [reason] (not_found|rate_limited).
|
||||||
|
GetFailedTotal metric.Int64Counter
|
||||||
|
|
||||||
|
// Successful setValue writes, with [user].
|
||||||
|
SetSuccessTotal metric.Int64Counter
|
||||||
|
|
||||||
|
// Failed setValue writes, with [reason] (bad_request|unauthorized).
|
||||||
|
SetFailedTotal metric.Int64Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newValueMetrics(meter metric.Meter) ValueMetrics {
|
||||||
|
getSuccessTotal, _ := meter.Int64Counter(
|
||||||
|
"codevault_get_value_success_total",
|
||||||
|
metric.WithDescription("Total number of successful getValue lookups"),
|
||||||
|
)
|
||||||
|
getFailedTotal, _ := meter.Int64Counter(
|
||||||
|
"codevault_get_value_failed_total",
|
||||||
|
metric.WithDescription("Total number of failed getValue lookups"),
|
||||||
|
)
|
||||||
|
setSuccessTotal, _ := meter.Int64Counter(
|
||||||
|
"codevault_set_value_success_total",
|
||||||
|
metric.WithDescription("Total number of successful setValue writes"),
|
||||||
|
)
|
||||||
|
setFailedTotal, _ := meter.Int64Counter(
|
||||||
|
"codevault_set_value_failed_total",
|
||||||
|
metric.WithDescription("Total number of failed setValue writes"),
|
||||||
|
)
|
||||||
|
return ValueMetrics{
|
||||||
|
GetSuccessTotal: getSuccessTotal,
|
||||||
|
GetFailedTotal: getFailedTotal,
|
||||||
|
SetSuccessTotal: setSuccessTotal,
|
||||||
|
SetFailedTotal: setFailedTotal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addObservableMetrics(meter metric.Meter) {
|
||||||
|
timeStarted := time.Now()
|
||||||
|
_, _ = meter.Int64ObservableGauge(
|
||||||
|
"startup_time",
|
||||||
|
metric.WithDescription("Startup timestamp"),
|
||||||
|
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
|
||||||
|
io.Observe(timeStarted.Unix(), metric.WithAttributes(attribute.String("version", internal.Version)))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
_, _ = meter.Int64ObservableGauge(
|
||||||
|
"last_update_time",
|
||||||
|
metric.WithDescription("Current timestamp"),
|
||||||
|
metric.WithInt64Callback(func(ctx context.Context, io metric.Int64Observer) error {
|
||||||
|
io.Observe(time.Now().Unix(), metric.WithAttributes(attribute.String("version", internal.Version)))
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
18
internal/observability/otel.go
Normal file
18
internal/observability/otel.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kanootoko/codevault/internal"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.43.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOtelResource() (*resource.Resource, error) {
|
||||||
|
return resource.Merge(
|
||||||
|
resource.Default(),
|
||||||
|
resource.NewWithAttributes(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceName(internal.PackageName),
|
||||||
|
semconv.ServiceVersion(internal.Version),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
145
internal/ratelimit/limiter.go
Normal file
145
internal/ratelimit/limiter.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Package ratelimit implements a per-IP sliding-window failure counter used to
|
||||||
|
// hide the database behind a cap on failed lookups.
|
||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bucket struct {
|
||||||
|
index int64
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipState struct {
|
||||||
|
current bucket
|
||||||
|
previous bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limiter is a two-bucket sliding-window counter per IP. Allowed is a pure read
|
||||||
|
// (RLock) that estimates the failure count in the trailing window from whatever
|
||||||
|
// is already stored, without mutating it. RecordFailure takes the write lock and
|
||||||
|
// performs the actual window roll and increment. Keeping the previous window's
|
||||||
|
// count means a client blocked near the end of one window stays throttled into
|
||||||
|
// the next, instead of getting an instant reset at the window boundary.
|
||||||
|
type Limiter struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
state map[string]ipState
|
||||||
|
window time.Duration
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLimiter(max int, window time.Duration) *Limiter {
|
||||||
|
return &Limiter{
|
||||||
|
state: make(map[string]ipState),
|
||||||
|
window: window,
|
||||||
|
max: max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Limiter) windowIndex(t time.Time) int64 {
|
||||||
|
return t.UnixNano() / int64(l.window)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowed reports whether ip is currently under the failure threshold. It never
|
||||||
|
// mutates the limiter's state.
|
||||||
|
func (l *Limiter) Allowed(ip string) bool {
|
||||||
|
now := time.Now()
|
||||||
|
idx := l.windowIndex(now)
|
||||||
|
|
||||||
|
l.mu.RLock()
|
||||||
|
state, ok := l.state[ip]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
current, previous := effectiveBuckets(state, idx)
|
||||||
|
|
||||||
|
overlap := overlapFraction(now, l.window, previous.index)
|
||||||
|
estimate := float64(current.count) + float64(previous.count)*overlap
|
||||||
|
|
||||||
|
return estimate < float64(l.max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordFailure increments the failure count for ip in the current window,
|
||||||
|
// rolling the previous stored window forward if it has become stale.
|
||||||
|
func (l *Limiter) RecordFailure(ip string) {
|
||||||
|
now := time.Now()
|
||||||
|
idx := l.windowIndex(now)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
state := l.state[ip]
|
||||||
|
current, previous := effectiveBuckets(state, idx)
|
||||||
|
current.count++
|
||||||
|
l.state[ip] = ipState{current: current, previous: previous}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes IPs whose state is more than 2 windows stale, bounding memory.
|
||||||
|
func (l *Limiter) Cleanup() {
|
||||||
|
idx := l.windowIndex(time.Now())
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
for ip, state := range l.state {
|
||||||
|
if idx-state.current.index > 2 {
|
||||||
|
delete(l.state, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run periodically calls Cleanup until ctx is done.
|
||||||
|
func (l *Limiter) Run(done <-chan struct{}, interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
l.Cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// effectiveBuckets derives the current/previous buckets as seen from window idx,
|
||||||
|
// without mutating the stored state: if the stored current bucket is stale by
|
||||||
|
// exactly one window it becomes the effective previous bucket; if it is older
|
||||||
|
// than that, both buckets are treated as empty.
|
||||||
|
func effectiveBuckets(state ipState, idx int64) (current, previous bucket) {
|
||||||
|
switch {
|
||||||
|
case state.current.index == idx:
|
||||||
|
current = state.current
|
||||||
|
if state.previous.index == idx-1 {
|
||||||
|
previous = state.previous
|
||||||
|
} else {
|
||||||
|
previous = bucket{index: idx - 1}
|
||||||
|
}
|
||||||
|
case state.current.index == idx-1:
|
||||||
|
current = bucket{index: idx}
|
||||||
|
previous = state.current
|
||||||
|
default:
|
||||||
|
current = bucket{index: idx}
|
||||||
|
previous = bucket{index: idx - 1}
|
||||||
|
}
|
||||||
|
return current, previous
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlapFraction returns how much of the previous window (identified by
|
||||||
|
// previousIdx) still falls within the trailing lookback window of length
|
||||||
|
// windowSize measured back from now.
|
||||||
|
func overlapFraction(now time.Time, windowSize time.Duration, previousIdx int64) float64 {
|
||||||
|
previousWindowEnd := time.Unix(0, (previousIdx+1)*int64(windowSize))
|
||||||
|
elapsedSincePreviousEnd := now.Sub(previousWindowEnd)
|
||||||
|
if elapsedSincePreviousEnd >= windowSize {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if elapsedSincePreviousEnd <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 1 - float64(elapsedSincePreviousEnd)/float64(windowSize)
|
||||||
|
}
|
||||||
101
internal/ratelimit/limiter_test.go
Normal file
101
internal/ratelimit/limiter_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package ratelimit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLimiter_AllowsUnderThreshold(t *testing.T) {
|
||||||
|
l := NewLimiter(10, time.Minute)
|
||||||
|
for i := range 9 {
|
||||||
|
if !l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected allowed at failure %d", i)
|
||||||
|
}
|
||||||
|
l.RecordFailure("1.2.3.4")
|
||||||
|
}
|
||||||
|
if !l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected allowed after 9 failures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_BlocksAtThreshold(t *testing.T) {
|
||||||
|
l := NewLimiter(10, time.Minute)
|
||||||
|
for range 10 {
|
||||||
|
l.RecordFailure("1.2.3.4")
|
||||||
|
}
|
||||||
|
if l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected blocked after 10 failures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_BlockedClientStaysThrottledAcrossWindowBoundary(t *testing.T) {
|
||||||
|
window := 300 * time.Millisecond
|
||||||
|
l := NewLimiter(10, window)
|
||||||
|
|
||||||
|
for range 20 {
|
||||||
|
l.RecordFailure("1.2.3.4")
|
||||||
|
}
|
||||||
|
if l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected blocked immediately after 20 failures")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := l.windowIndex(time.Now())
|
||||||
|
windowEnd := time.Unix(0, (idx+1)*int64(window))
|
||||||
|
// Sleep just past the window boundary so the failures roll into the previous
|
||||||
|
// bucket, then check that their weighted contribution still blocks the client.
|
||||||
|
time.Sleep(time.Until(windowEnd) + window/10)
|
||||||
|
|
||||||
|
if l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected still blocked shortly after the window boundary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_ResetsAfterIdlePeriod(t *testing.T) {
|
||||||
|
window := 50 * time.Millisecond
|
||||||
|
l := NewLimiter(10, window)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
l.RecordFailure("1.2.3.4")
|
||||||
|
}
|
||||||
|
if l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected blocked immediately after 10 failures")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(window * 3)
|
||||||
|
|
||||||
|
if !l.Allowed("1.2.3.4") {
|
||||||
|
t.Fatalf("expected allowed after idling for 2+ windows")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLimiter_Cleanup(t *testing.T) {
|
||||||
|
window := 20 * time.Millisecond
|
||||||
|
l := NewLimiter(10, window)
|
||||||
|
l.RecordFailure("1.2.3.4")
|
||||||
|
|
||||||
|
time.Sleep(window * 4)
|
||||||
|
l.Cleanup()
|
||||||
|
|
||||||
|
l.mu.RLock()
|
||||||
|
_, ok := l.state["1.2.3.4"]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
t.Fatalf("expected stale IP to be cleaned up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverlapFraction(t *testing.T) {
|
||||||
|
window := 100 * time.Millisecond
|
||||||
|
previousIdx := int64(1000)
|
||||||
|
previousWindowEnd := time.Unix(0, (previousIdx+1)*int64(window))
|
||||||
|
|
||||||
|
if got := overlapFraction(previousWindowEnd.Add(-10*time.Millisecond), window, previousIdx); got != 1 {
|
||||||
|
t.Fatalf("expected overlap 1 before window end, got %v", got)
|
||||||
|
}
|
||||||
|
if got := overlapFraction(previousWindowEnd.Add(20*time.Millisecond), window, previousIdx); got < 0.79 || got > 0.81 {
|
||||||
|
t.Fatalf("expected overlap ~0.8 at 20ms into next window, got %v", got)
|
||||||
|
}
|
||||||
|
if got := overlapFraction(previousWindowEnd.Add(200*time.Millisecond), window, previousIdx); got != 0 {
|
||||||
|
t.Fatalf("expected overlap 0 once fully elapsed, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
internal/runcmd/add_user.go
Normal file
32
internal/runcmd/add_user.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package runcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/auth"
|
||||||
|
"github.com/kanootoko/codevault/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddUser opens (creating if necessary) the database at dbPath and upserts a
|
||||||
|
// user with the given login/password. It intentionally does not depend on the
|
||||||
|
// application config.
|
||||||
|
func AddUser(dbPath, login, password string) error {
|
||||||
|
database, err := db.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on opening database: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = database.Close() }()
|
||||||
|
|
||||||
|
hash, err := auth.Hash(login, password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on hashing password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users := db.NewUsersRepository(database)
|
||||||
|
if err := users.Upsert(context.Background(), login, hash); err != nil {
|
||||||
|
return fmt.Errorf("error on upserting user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
59
internal/runcmd/config_example.go
Normal file
59
internal/runcmd/config_example.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package runcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConfigExample(configPath string) error {
|
||||||
|
configExample := config.Config{
|
||||||
|
Environment: config.DevelopmentEnvironment,
|
||||||
|
Server: config.ServerConfig{
|
||||||
|
Address: ":8080",
|
||||||
|
// Example: a Docker-network CIDR in front of a reverse proxy nginx.
|
||||||
|
// Replace with the actual proxy's IP/CIDR, or leave empty to trust
|
||||||
|
// no forwarded-for headers.
|
||||||
|
TrustedProxies: []string{"172.16.0.0/12"},
|
||||||
|
},
|
||||||
|
Database: config.DatabaseConfig{
|
||||||
|
Path: "./codevault.db",
|
||||||
|
},
|
||||||
|
RateLimit: config.RateLimitConfig{
|
||||||
|
MaxFailures: 10,
|
||||||
|
Window: time.Minute,
|
||||||
|
},
|
||||||
|
Observability: config.ObservabilityConfig{
|
||||||
|
Logging: config.LoggingConfig{
|
||||||
|
Stderr: config.InfoLoggingLevel,
|
||||||
|
Files: map[config.LoggingLevel]string{config.InfoLoggingLevel: "./info.log"},
|
||||||
|
Exporter: config.LogsExporterConfig{
|
||||||
|
Level: config.InfoLoggingLevel,
|
||||||
|
Endpoint: "http://127.0.0.1:4317/v1/logs",
|
||||||
|
Transport: "grpc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metrics: config.MetricsConfig{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 9092,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
configBytesBuffer := bytes.NewBuffer(nil)
|
||||||
|
encoder := yaml.NewEncoder(configBytesBuffer)
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
err := encoder.Encode(configExample)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot marshal example config: %w", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(configPath, configBytesBuffer.Bytes(), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot write config file: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
internal/runcmd/run.go
Normal file
54
internal/runcmd/run.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package runcmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/kanootoko/codevault/internal/config"
|
||||||
|
"github.com/kanootoko/codevault/internal/db"
|
||||||
|
"github.com/kanootoko/codevault/internal/httpserver"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/logging"
|
||||||
|
"github.com/kanootoko/codevault/internal/observability/metrics"
|
||||||
|
"github.com/kanootoko/codevault/internal/ratelimit"
|
||||||
|
"github.com/kanootoko/codevault/internal/utils/gshutdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunServer(ctx context.Context, appConfig config.Config) error {
|
||||||
|
logger, err := logging.LoggerFromCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on getting logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
appMetrics := metrics.NewMetrics(metrics.GetMeter())
|
||||||
|
|
||||||
|
database, err := db.Open(appConfig.Database.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on opening database: %w", err)
|
||||||
|
}
|
||||||
|
_ = gshutdown.SubtaskStarted(ctx, "database")
|
||||||
|
go func() {
|
||||||
|
defer func() { _ = gshutdown.SubtaskFinished(ctx, "database") }()
|
||||||
|
<-ctx.Done()
|
||||||
|
if err := database.Close(); err != nil {
|
||||||
|
logger.Error("error on closing database", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
secrets := db.NewSecretsRepository(database)
|
||||||
|
users := db.NewUsersRepository(database)
|
||||||
|
|
||||||
|
limiter := ratelimit.NewLimiter(appConfig.RateLimit.MaxFailures, appConfig.RateLimit.Window)
|
||||||
|
|
||||||
|
router, err := httpserver.New(secrets, users, limiter, appMetrics, appConfig.Server.TrustedProxies)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error on building http router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("starting codevault server")
|
||||||
|
if err := httpserver.Run(ctx, appConfig.Server.Address, router, limiter); err != nil {
|
||||||
|
return fmt.Errorf("error on running http server: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
17
internal/utils/env.go
Normal file
17
internal/utils/env.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TryFlagFromEnv(cmd *cobra.Command, flag string, envvar string, required bool) {
|
||||||
|
if value, ok := os.LookupEnv(envvar); ok {
|
||||||
|
_ = cmd.Flags().Set(flag, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if required {
|
||||||
|
cmd.MarkFlagRequired(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
174
internal/utils/gshutdown/graceful_shutdown.go
Normal file
174
internal/utils/gshutdown/graceful_shutdown.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package gshutdown
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotInitialized = errors.New("seems that graceful shutdown has not been initialized")
|
||||||
|
ErrTimeout = errors.New("timeout reached")
|
||||||
|
ErrContextFinished = errors.New("context is finished, unable to add subtasks")
|
||||||
|
ErrContextNotFinished = errors.New("context is not finished, unable to await subtasks")
|
||||||
|
)
|
||||||
|
|
||||||
|
func CtxWithGracefulShutdown(ctx context.Context, logger *zap.Logger) context.Context {
|
||||||
|
if _, ok := ctx.Value(gracefulShutdownKey).(*gracefulShutdownData); ok {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.L().Named("graceful_shutdown")
|
||||||
|
}
|
||||||
|
data := gracefulShutdownData{
|
||||||
|
subtasks: make(map[string]int),
|
||||||
|
operationsChan: make(chan operation, 1),
|
||||||
|
finishedChan: make(chan struct{}),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, gracefulShutdownKey, &data)
|
||||||
|
|
||||||
|
go data.run(ctx)
|
||||||
|
data.logger.Info("initialized")
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubtaskStarted(ctx context.Context, subtaskName string) error {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ErrContextFinished
|
||||||
|
}
|
||||||
|
gs, ok := ctx.Value(gracefulShutdownKey).(*gracefulShutdownData)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotInitialized
|
||||||
|
}
|
||||||
|
gs.operationsChan <- operation{Type: startOperationType, SubtaskName: subtaskName}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubtaskFinished(ctx context.Context, subtaskName string) error {
|
||||||
|
gs, ok := ctx.Value(gracefulShutdownKey).(*gracefulShutdownData)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotInitialized
|
||||||
|
}
|
||||||
|
gs.operationsChan <- operation{Type: finishOperationType, SubtaskName: subtaskName}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AwaitWithTimeout(ctx context.Context, timeout time.Duration) error {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
return fmt.Errorf("context is not finished yet")
|
||||||
|
}
|
||||||
|
gs, ok := ctx.Value(gracefulShutdownKey).(*gracefulShutdownData)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotInitialized
|
||||||
|
}
|
||||||
|
timer := time.NewTimer(timeout)
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
return ErrTimeout
|
||||||
|
case <-gs.finishedChan:
|
||||||
|
return gs.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type gracefulShutdownKeyType int
|
||||||
|
|
||||||
|
const gracefulShutdownKey gracefulShutdownKeyType = iota
|
||||||
|
|
||||||
|
type gracefulShutdownData struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
subtasks map[string]int
|
||||||
|
operationsChan chan operation
|
||||||
|
finishedChan chan struct{}
|
||||||
|
err error
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type operationType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
startOperationType operationType = "start"
|
||||||
|
finishOperationType operationType = "finish"
|
||||||
|
)
|
||||||
|
|
||||||
|
type operation struct {
|
||||||
|
Type operationType
|
||||||
|
SubtaskName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *gracefulShutdownData) run(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if data.isDone() {
|
||||||
|
close(data.finishedChan)
|
||||||
|
data.logger.Info("no tasks to be awaited")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case op := <-data.operationsChan:
|
||||||
|
switch op.Type {
|
||||||
|
case startOperationType:
|
||||||
|
data.acknowledgeSubtask(op.SubtaskName)
|
||||||
|
default:
|
||||||
|
data.finishSubtask(op.SubtaskName)
|
||||||
|
}
|
||||||
|
if data.isDone() {
|
||||||
|
close(data.finishedChan)
|
||||||
|
data.logger.Info("all tasks successfully awaited")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.logger.Debug("awaiting in progress", zap.Strings("to_be_awaited", data.toBeAwaited()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *gracefulShutdownData) acknowledgeSubtask(subtaskName string) {
|
||||||
|
data.mu.Lock()
|
||||||
|
defer data.mu.Unlock()
|
||||||
|
|
||||||
|
data.subtasks[subtaskName]++
|
||||||
|
data.logger.Info(
|
||||||
|
"successful acknowledge",
|
||||||
|
zap.String("subtask", subtaskName),
|
||||||
|
zap.Int("total_count", data.subtasks[subtaskName]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *gracefulShutdownData) finishSubtask(subtaskName string) {
|
||||||
|
data.mu.Lock()
|
||||||
|
defer data.mu.Unlock()
|
||||||
|
|
||||||
|
data.subtasks[subtaskName]--
|
||||||
|
data.logger.Info(
|
||||||
|
"successful finish",
|
||||||
|
zap.String("subtask", subtaskName),
|
||||||
|
zap.Int("left_count", data.subtasks[subtaskName]),
|
||||||
|
)
|
||||||
|
if data.subtasks[subtaskName] == 0 {
|
||||||
|
delete(data.subtasks, subtaskName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *gracefulShutdownData) isDone() bool {
|
||||||
|
data.mu.Lock()
|
||||||
|
defer data.mu.Unlock()
|
||||||
|
return len(data.subtasks) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *gracefulShutdownData) toBeAwaited() []string {
|
||||||
|
data.mu.Lock()
|
||||||
|
defer data.mu.Unlock()
|
||||||
|
|
||||||
|
res := make([]string, 0, len(data.subtasks))
|
||||||
|
for subtaskName, count := range data.subtasks {
|
||||||
|
for range count {
|
||||||
|
res = append(res, subtaskName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
4
internal/version.go
Normal file
4
internal/version.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
const Version = "0.1.0"
|
||||||
|
const PackageName = "codevault"
|
||||||
12
internal/webui/embed.go
Normal file
12
internal/webui/embed.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Package webui embeds the static HTML+JS pages served for the get/set flows.
|
||||||
|
package webui
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed static/get.html static/set.html
|
||||||
|
var Static embed.FS
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetPagePath = "static/get.html"
|
||||||
|
SetPagePath = "static/set.html"
|
||||||
|
)
|
||||||
48
internal/webui/static/get.html
Normal file
48
internal/webui/static/get.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>CodeVault - Get value</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 480px; margin: 4rem auto; }
|
||||||
|
input, button { font-size: 1rem; padding: 0.4rem; }
|
||||||
|
input { width: 100%; box-sizing: border-box; margin-bottom: 0.75rem; }
|
||||||
|
#result { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
#result.ok { background: #e6ffed; border: 1px solid #4caf50; }
|
||||||
|
#result.err { background: #ffecec; border: 1px solid #e53935; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Get value by code</h1>
|
||||||
|
<input id="code" type="text" placeholder="Access code" autofocus>
|
||||||
|
<button id="submit">Get value</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
<p><a href="/set">Set a value instead</a></p>
|
||||||
|
<script>
|
||||||
|
document.getElementById('submit').addEventListener('click', async () => {
|
||||||
|
const code = document.getElementById('code').value;
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.className = '';
|
||||||
|
result.textContent = '';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/getValue', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ code }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
result.className = 'ok';
|
||||||
|
result.textContent = data.value;
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
result.className = 'err';
|
||||||
|
result.textContent = 'Not found';
|
||||||
|
} else {
|
||||||
|
result.className = 'err';
|
||||||
|
result.textContent = 'Error: ' + response.status;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
internal/webui/static/set.html
Normal file
59
internal/webui/static/set.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>CodeVault - Set value</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 480px; margin: 4rem auto; }
|
||||||
|
input, button, textarea { font-size: 1rem; padding: 0.4rem; }
|
||||||
|
input, textarea { width: 100%; box-sizing: border-box; margin-bottom: 0.75rem; }
|
||||||
|
#result { margin-top: 1rem; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-break: break-all; }
|
||||||
|
#result.ok { background: #e6ffed; border: 1px solid #4caf50; }
|
||||||
|
#result.err { background: #ffecec; border: 1px solid #e53935; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Set value for a code</h1>
|
||||||
|
<input id="login" type="text" placeholder="Login" autocomplete="username">
|
||||||
|
<input id="password" type="password" placeholder="Password" autocomplete="current-password">
|
||||||
|
<input id="code" type="text" placeholder="Access code">
|
||||||
|
<textarea id="value" rows="4" placeholder="Value"></textarea>
|
||||||
|
<button id="submit">Save</button>
|
||||||
|
<div id="result"></div>
|
||||||
|
<p><a href="/">Get a value instead</a></p>
|
||||||
|
<script>
|
||||||
|
document.getElementById('submit').addEventListener('click', async () => {
|
||||||
|
const login = document.getElementById('login').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const code = document.getElementById('code').value;
|
||||||
|
const value = document.getElementById('value').value;
|
||||||
|
const result = document.getElementById('result');
|
||||||
|
result.className = '';
|
||||||
|
result.textContent = '';
|
||||||
|
|
||||||
|
const response = await fetch('/api/v1/setValue', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + btoa(login + ':' + password),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code, value }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
result.className = 'ok';
|
||||||
|
result.textContent = 'Saved';
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
result.className = 'err';
|
||||||
|
result.textContent = 'Invalid login or password';
|
||||||
|
} else if (response.status === 400) {
|
||||||
|
result.className = 'err';
|
||||||
|
result.textContent = 'Code and value are required';
|
||||||
|
} else {
|
||||||
|
result.className = 'err';
|
||||||
|
result.textContent = 'Error: ' + response.status;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user