diff --git a/go/.golangci.yaml b/go/.golangci.yaml index 62b43e6c41..13443bfd56 100644 --- a/go/.golangci.yaml +++ b/go/.golangci.yaml @@ -69,7 +69,7 @@ linters-settings: - "^github.com/urfave/cli/v2.*$" lll: - line-length: 100 + line-length: 120 funlen: # Checks the number of lines in a function. # If lower than 0, disable the check. @@ -188,6 +188,11 @@ linters-settings: packages: - github.com/jmoiron/sqlx + wrapcheck: + extra-ignore-sigs: + - fault.New( + ignoreSigRegexps: + - mw\.next\..* sloglint: # Enforce not using global loggers. # Values: @@ -254,7 +259,7 @@ linters: # - goprintffuncname # checks that printf-like functions are named with f at the end - gosec # inspects source code for security problems # - intrange # finds places where for loops could make use of an integer range - - lll # reports long lines + # - lll # reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) # - makezero # finds slice declarations with non-zero initial length # - mirror # reports wrong mirror patterns of bytes/strings usage diff --git a/go/api/gen.go b/go/api/gen.go index 823db084a6..8ffa88aff5 100644 --- a/go/api/gen.go +++ b/go/api/gen.go @@ -1,20 +1,8 @@ -//go:build go1.22 - -// Package openapi provides primitives to interact with the openapi HTTP API. +// Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT. package api -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/oapi-codegen/runtime" - strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" -) - // BadRequestError defines model for BadRequestError. type BadRequestError struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -66,6 +54,9 @@ type InternalServerError = BaseError // NotFoundError defines model for NotFoundError. type NotFoundError = BaseError +// PreconditionFailedError defines model for PreconditionFailedError. +type PreconditionFailedError = BaseError + // V2LivenessResponseBody defines model for V2LivenessResponseBody. type V2LivenessResponseBody struct { // Message Whether we're alive or not @@ -152,466 +143,3 @@ type V1RatelimitLimitJSONRequestBody = V2RatelimitLimitRequestBody // V2RatelimitSetOverrideJSONRequestBody defines body for V2RatelimitSetOverride for application/json ContentType. type V2RatelimitSetOverrideJSONRequestBody = V2RatelimitSetOverrideRequestBody - -// ServerInterface represents all server handlers. -type ServerInterface interface { - // Liveness check - // (GET /v2/liveness) - Liveness(w http.ResponseWriter, r *http.Request) - - // (POST /v2/ratelimit.limit) - V1RatelimitLimit(w http.ResponseWriter, r *http.Request, params V1RatelimitLimitParams) - - // (POST /v2/ratelimit.setOverride) - V2RatelimitSetOverride(w http.ResponseWriter, r *http.Request) -} - -// ServerInterfaceWrapper converts contexts to parameters. -type ServerInterfaceWrapper struct { - Handler ServerInterface - HandlerMiddlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -type MiddlewareFunc func(http.Handler) http.Handler - -// Liveness operation middleware -func (siw *ServerInterfaceWrapper) Liveness(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.Liveness(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// V1RatelimitLimit operation middleware -func (siw *ServerInterfaceWrapper) V1RatelimitLimit(w http.ResponseWriter, r *http.Request) { - - var err error - - // Parameter object where we will unmarshal all parameters from the context - var params V1RatelimitLimitParams - - // ------------- Optional query parameter "xxx" ------------- - - err = runtime.BindQueryParameter("form", true, false, "xxx", r.URL.Query(), ¶ms.Xxx) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "xxx", Err: err}) - return - } - - // ------------- Required query parameter "yyy" ------------- - - if paramValue := r.URL.Query().Get("yyy"); paramValue != "" { - - } else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "yyy"}) - return - } - - err = runtime.BindQueryParameter("form", true, true, "yyy", r.URL.Query(), ¶ms.Yyy) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "yyy", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.V1RatelimitLimit(w, r, params) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -// V2RatelimitSetOverride operation middleware -func (siw *ServerInterfaceWrapper) V2RatelimitSetOverride(w http.ResponseWriter, r *http.Request) { - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.V2RatelimitSetOverride(w, r) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - -type UnescapedCookieParamError struct { - ParamName string - Err error -} - -func (e *UnescapedCookieParamError) Error() string { - return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) -} - -func (e *UnescapedCookieParamError) Unwrap() error { - return e.Err -} - -type UnmarshalingParamError struct { - ParamName string - Err error -} - -func (e *UnmarshalingParamError) Error() string { - return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) -} - -func (e *UnmarshalingParamError) Unwrap() error { - return e.Err -} - -type RequiredParamError struct { - ParamName string -} - -func (e *RequiredParamError) Error() string { - return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) -} - -type RequiredHeaderError struct { - ParamName string - Err error -} - -func (e *RequiredHeaderError) Error() string { - return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) -} - -func (e *RequiredHeaderError) Unwrap() error { - return e.Err -} - -type InvalidParamFormatError struct { - ParamName string - Err error -} - -func (e *InvalidParamFormatError) Error() string { - return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) -} - -func (e *InvalidParamFormatError) Unwrap() error { - return e.Err -} - -type TooManyValuesForParamError struct { - ParamName string - Count int -} - -func (e *TooManyValuesForParamError) Error() string { - return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) -} - -// Handler creates http.Handler with routing matching OpenAPI spec. -func Handler(si ServerInterface) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions{}) -} - -// ServeMux is an abstraction of http.ServeMux. -type ServeMux interface { - HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) - ServeHTTP(w http.ResponseWriter, r *http.Request) -} - -type StdHTTPServerOptions struct { - BaseURL string - BaseRouter ServeMux - Middlewares []MiddlewareFunc - ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. -func HandlerFromMux(si ServerInterface, m ServeMux) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions{ - BaseRouter: m, - }) -} - -func HandlerFromMuxWithBaseURL(si ServerInterface, m ServeMux, baseURL string) http.Handler { - return HandlerWithOptions(si, StdHTTPServerOptions{ - BaseURL: baseURL, - BaseRouter: m, - }) -} - -// HandlerWithOptions creates http.Handler with additional options -func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.Handler { - m := options.BaseRouter - - if m == nil { - m = http.NewServeMux() - } - if options.ErrorHandlerFunc == nil { - options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - } - } - - wrapper := ServerInterfaceWrapper{ - Handler: si, - HandlerMiddlewares: options.Middlewares, - ErrorHandlerFunc: options.ErrorHandlerFunc, - } - - m.HandleFunc("GET "+options.BaseURL+"/v2/liveness", wrapper.Liveness) - m.HandleFunc("POST "+options.BaseURL+"/v2/ratelimit.limit", wrapper.V1RatelimitLimit) - m.HandleFunc("POST "+options.BaseURL+"/v2/ratelimit.setOverride", wrapper.V2RatelimitSetOverride) - - return m -} - -type LivenessRequestObject struct { -} - -type LivenessResponseObject interface { - VisitLivenessResponse(w http.ResponseWriter) error -} - -type Liveness200JSONResponse V2LivenessResponseBody - -func (response Liveness200JSONResponse) VisitLivenessResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type Liveness500JSONResponse InternalServerError - -func (response Liveness500JSONResponse) VisitLivenessResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - - return json.NewEncoder(w).Encode(response) -} - -type V1RatelimitLimitRequestObject struct { - Params V1RatelimitLimitParams - Body *V1RatelimitLimitJSONRequestBody -} - -type V1RatelimitLimitResponseObject interface { - VisitV1RatelimitLimitResponse(w http.ResponseWriter) error -} - -type V1RatelimitLimit200JSONResponse V2RatelimitLimitResponseBody - -func (response V1RatelimitLimit200JSONResponse) VisitV1RatelimitLimitResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type V1RatelimitLimit400JSONResponse BadRequestError - -func (response V1RatelimitLimit400JSONResponse) VisitV1RatelimitLimitResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type V1RatelimitLimit404JSONResponse NotFoundError - -func (response V1RatelimitLimit404JSONResponse) VisitV1RatelimitLimitResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - - return json.NewEncoder(w).Encode(response) -} - -type V1RatelimitLimit500JSONResponse InternalServerError - -func (response V1RatelimitLimit500JSONResponse) VisitV1RatelimitLimitResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - - return json.NewEncoder(w).Encode(response) -} - -type V2RatelimitSetOverrideRequestObject struct { - Body *V2RatelimitSetOverrideJSONRequestBody -} - -type V2RatelimitSetOverrideResponseObject interface { - VisitV2RatelimitSetOverrideResponse(w http.ResponseWriter) error -} - -type V2RatelimitSetOverride200JSONResponse V2RatelimitSetOverrideResponseBody - -func (response V2RatelimitSetOverride200JSONResponse) VisitV2RatelimitSetOverrideResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - - return json.NewEncoder(w).Encode(response) -} - -type V2RatelimitSetOverride400JSONResponse BadRequestError - -func (response V2RatelimitSetOverride400JSONResponse) VisitV2RatelimitSetOverrideResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) - - return json.NewEncoder(w).Encode(response) -} - -type V2RatelimitSetOverride404JSONResponse NotFoundError - -func (response V2RatelimitSetOverride404JSONResponse) VisitV2RatelimitSetOverrideResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) - - return json.NewEncoder(w).Encode(response) -} - -type V2RatelimitSetOverride500JSONResponse InternalServerError - -func (response V2RatelimitSetOverride500JSONResponse) VisitV2RatelimitSetOverrideResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - - return json.NewEncoder(w).Encode(response) -} - -// StrictServerInterface represents all server handlers. -type StrictServerInterface interface { - // Liveness check - // (GET /v2/liveness) - Liveness(ctx context.Context, request LivenessRequestObject) (LivenessResponseObject, error) - - // (POST /v2/ratelimit.limit) - V1RatelimitLimit(ctx context.Context, request V1RatelimitLimitRequestObject) (V1RatelimitLimitResponseObject, error) - - // (POST /v2/ratelimit.setOverride) - V2RatelimitSetOverride(ctx context.Context, request V2RatelimitSetOverrideRequestObject) (V2RatelimitSetOverrideResponseObject, error) -} - -type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc -type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc - -type StrictHTTPServerOptions struct { - RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) - ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) -} - -func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ - RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusBadRequest) - }, - ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { - http.Error(w, err.Error(), http.StatusInternalServerError) - }, - }} -} - -func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { - return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} -} - -type strictHandler struct { - ssi StrictServerInterface - middlewares []StrictMiddlewareFunc - options StrictHTTPServerOptions -} - -// Liveness operation middleware -func (sh *strictHandler) Liveness(w http.ResponseWriter, r *http.Request) { - var request LivenessRequestObject - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.Liveness(ctx, request.(LivenessRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "Liveness") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(LivenessResponseObject); ok { - if err := validResponse.VisitLivenessResponse(w); err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } - } else if response != nil { - sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) - } -} - -// V1RatelimitLimit operation middleware -func (sh *strictHandler) V1RatelimitLimit(w http.ResponseWriter, r *http.Request, params V1RatelimitLimitParams) { - var request V1RatelimitLimitRequestObject - - request.Params = params - - var body V1RatelimitLimitJSONRequestBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - request.Body = &body - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.V1RatelimitLimit(ctx, request.(V1RatelimitLimitRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "V1RatelimitLimit") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(V1RatelimitLimitResponseObject); ok { - if err := validResponse.VisitV1RatelimitLimitResponse(w); err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } - } else if response != nil { - sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) - } -} - -// V2RatelimitSetOverride operation middleware -func (sh *strictHandler) V2RatelimitSetOverride(w http.ResponseWriter, r *http.Request) { - var request V2RatelimitSetOverrideRequestObject - - var body V2RatelimitSetOverrideJSONRequestBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) - return - } - request.Body = &body - - handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { - return sh.ssi.V2RatelimitSetOverride(ctx, request.(V2RatelimitSetOverrideRequestObject)) - } - for _, middleware := range sh.middlewares { - handler = middleware(handler, "V2RatelimitSetOverride") - } - - response, err := handler(r.Context(), w, r, request) - - if err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } else if validResponse, ok := response.(V2RatelimitSetOverrideResponseObject); ok { - if err := validResponse.VisitV2RatelimitSetOverrideResponse(w); err != nil { - sh.options.ResponseErrorHandlerFunc(w, r, err) - } - } else if response != nil { - sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) - } -} diff --git a/go/api/openapi.json b/go/api/openapi.json index ce8db7bf5c..c2fcd762af 100644 --- a/go/api/openapi.json +++ b/go/api/openapi.json @@ -56,6 +56,12 @@ "NotFoundError": { "$ref": "#/components/schemas/BaseError" }, + "ForbiddenError": { + "$ref": "#/components/schemas/BaseError" + }, + "PreconditionFailedError": { + "$ref": "#/components/schemas/BaseError" + }, "BadRequestError": { "allOf": [ { @@ -357,6 +363,16 @@ }, "description": "OK" }, + "412": { + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/PreconditionFailedError" + } + } + }, + "description": "Internal Server Error" + }, "500": { "content": { "application/problem+json": { diff --git a/go/cmd/main.go b/go/cmd/main.go index 3318a6ac14..ec37872ebf 100644 --- a/go/cmd/main.go +++ b/go/cmd/main.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/Southclaws/fault" "github.com/unkeyed/unkey/go/cmd/api" + "github.com/unkeyed/unkey/go/pkg/fault" "github.com/urfave/cli/v2" ) diff --git a/go/go.mod b/go/go.mod index 2907da55bf..ac3b1d617a 100644 --- a/go/go.mod +++ b/go/go.mod @@ -4,43 +4,57 @@ go 1.23.4 require ( github.com/ClickHouse/clickhouse-go/v2 v2.28.1 - github.com/Southclaws/fault v0.8.1 + github.com/axiomhq/axiom-go v0.20.2 github.com/btcsuite/btcutil v1.0.2 github.com/danielgtaylor/huma v1.14.2 + github.com/go-sql-driver/mysql v1.8.1 github.com/google/uuid v1.6.0 github.com/lmittmann/tint v1.0.6 + github.com/maypok86/otter v1.2.2 github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 - github.com/oapi-codegen/runtime v1.1.1 + github.com/ory/dockertest/v3 v3.11.0 github.com/pb33f/libopenapi v0.16.5 github.com/pb33f/libopenapi-validator v0.1.0 github.com/segmentio/ksuid v1.0.4 github.com/sqlc-dev/sqlc v1.28.0 github.com/stretchr/testify v1.10.0 - github.com/unkeyed/unkey/apps/agent v0.0.0-20250104094322-474d5d220db9 + github.com/unkeyed/unkey/apps/agent v0.0.0-20250124105149-f6d38cdef22e github.com/urfave/cli/v2 v2.27.4 github.com/xeipuuv/gojsonschema v1.2.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 ) require ( cel.dev/expr v0.18.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/ClickHouse/ch-go v0.62.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/Southclaws/fault v0.8.1 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/axiomhq/axiom-go v0.20.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v27.2.0+incompatible // indirect + github.com/docker/docker v27.2.0+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dolthub/maphash v0.1.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gammazero/deque v0.2.1 // indirect github.com/getkin/kin-openapi v0.127.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect @@ -48,10 +62,12 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/cel-go v0.22.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect @@ -65,11 +81,16 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.36.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.13 // indirect github.com/paulmach/orb v0.11.1 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pganalyze/pg_query_go/v5 v5.1.0 // indirect @@ -92,6 +113,7 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -105,12 +127,10 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect go.opentelemetry.io/otel/sdk v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go/go.sum b/go/go.sum index 00be548096..0b3aeb6cd2 100644 --- a/go/go.sum +++ b/go/go.sum @@ -49,9 +49,13 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/ch-go v0.62.0 h1:eXH0hytXeCEEZHgMvOX9IiW7wqBb4w1MJMp9rArbkrc= @@ -60,8 +64,11 @@ github.com/ClickHouse/clickhouse-go/v2 v2.28.1 h1:tpdOxWZlZ4IYiFWpIteU57JVdWVbSI github.com/ClickHouse/clickhouse-go/v2 v2.28.1/go.mod h1:0U915l9qynE508ehh3ea9+UMGc7gZlAV+9W6pUZd7kk= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Jeffail/gabs/v2 v2.6.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Southclaws/fault v0.8.1 h1:mgqqdC6kUBQ6ExMALZ0nNaDfNJD5h2+wq3se5mAyX+8= github.com/Southclaws/fault v0.8.1/go.mod h1:VUVkAWutC59SL16s6FTqf3I6I2z77RmnaW5XRz4bLOE= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -75,8 +82,6 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= @@ -93,7 +98,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -131,12 +135,16 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/danielgtaylor/casing v0.0.0-20210126043903-4e55e6373ac3/go.mod h1:eFdYmNxcuLDrRNW0efVoxSaApmvGXfHZ9k2CT/RSUF0= @@ -147,6 +155,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v27.2.0+incompatible h1:yHD1QEB1/0vr5eBNpu8tncu8gWxg8EydFPOSKHzXSMM= +github.com/docker/cli v27.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= +github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= @@ -179,6 +197,8 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= +github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -213,9 +233,12 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -296,6 +319,8 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -367,7 +392,6 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -387,7 +411,11 @@ 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.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc= github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= @@ -414,6 +442,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/maypok86/otter v1.2.2 h1:jJi0y8ruR/ZcKmJ4FbQj3QQTqKwV+LNrSOo2S1zbF5M= +github.com/maypok86/otter v1.2.2/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= @@ -422,6 +452,10 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -440,8 +474,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q= github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8= -github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= -github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -458,7 +490,15 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= @@ -544,6 +584,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= @@ -558,7 +600,6 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= -github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/sqlc-dev/sqlc v1.28.0 h1:2QB4X22pKNpKMyb8dRLnqZwMXW6S+ZCyYCpa+3/ICcI= github.com/sqlc-dev/sqlc v1.28.0/go.mod h1:x6wDsOHH60dTX3ES9sUUxRVaROg5aFB3l3nkkjyuK1A= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= @@ -589,8 +630,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unkeyed/unkey/apps/agent v0.0.0-20250104094322-474d5d220db9 h1:tmZvgFuA3FfBVcfAqRK+EpM9MFzup7imdyB9ywrAk+Q= -github.com/unkeyed/unkey/apps/agent v0.0.0-20250104094322-474d5d220db9/go.mod h1:DT9bY3agxOekRiikkvtXWqlUpRoyDqL0RYWQECD59SI= +github.com/unkeyed/unkey/apps/agent v0.0.0-20250124105149-f6d38cdef22e h1:WHBZyYpuJatvqk1Qz12+pOkiv8nLsiaC4gGstmpMlwI= +github.com/unkeyed/unkey/apps/agent v0.0.0-20250124105149-f6d38cdef22e/go.mod h1:ogkfx3pi5Gf7JDt1+Y4Z+nyU87Kx/J31kqOYu+AO+Fk= github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= @@ -875,6 +916,7 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1142,6 +1184,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go/hash/sha256.go b/go/hash/sha256.go new file mode 100644 index 0000000000..db634d4383 --- /dev/null +++ b/go/hash/sha256.go @@ -0,0 +1,13 @@ +package hash + +import ( + "crypto/sha256" + "encoding/base64" +) + +func Sha256(s string) string { + hash := sha256.New() + hash.Write([]byte(s)) + + return base64.StdEncoding.EncodeToString(hash.Sum(nil)) +} diff --git a/go/hash/sha256_test.go b/go/hash/sha256_test.go new file mode 100644 index 0000000000..dd163fae99 --- /dev/null +++ b/go/hash/sha256_test.go @@ -0,0 +1,22 @@ +package hash + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSha256(t *testing.T) { + for i := 0; i < 100; i++ { + b := []byte{32} + _, err := rand.Read(b) + require.NoError(t, err) + s := string(b) + h := Sha256(s) + require.Greater(t, len(h), 10) + + // check if it's consistent + require.Equal(t, h, Sha256(s)) + } +} diff --git a/go/pkg/cache/cache.go b/go/pkg/cache/cache.go new file mode 100644 index 0000000000..838513ffe8 --- /dev/null +++ b/go/pkg/cache/cache.go @@ -0,0 +1,223 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "time" + + "github.com/maypok86/otter" + "github.com/unkeyed/unkey/go/pkg/clock" + "github.com/unkeyed/unkey/go/pkg/fault" + "github.com/unkeyed/unkey/go/pkg/logging" + "github.com/unkeyed/unkey/go/pkg/tracing" + "go.opentelemetry.io/otel/attribute" +) + +type cache[T any] struct { + otter otter.Cache[string, swrEntry[T]] + fresh time.Duration + stale time.Duration + refreshFromOrigin func(ctx context.Context, identifier string) (data T, ok bool) + // If a key is stale, its identifier will be put into this channel and a goroutine refreshes it in the background + refreshC chan string + logger logging.Logger + resource string + clock clock.Clock +} + +type Config[T any] struct { + // How long the data is considered fresh + // Subsequent requests in this time will try to use the cache + Fresh time.Duration + + // Subsequent requests that are not fresh but within the stale time will return cached data but also trigger + // fetching from the origin server + Stale time.Duration + + // A handler that will be called to refetch data from the origin when necessary + RefreshFromOrigin func(ctx context.Context, identifier string) (data T, ok bool) + + Logger logging.Logger + + // Start evicting the least recently used entry when the cache grows to MaxSize + MaxSize int + + Resource string + + Clock clock.Clock +} + +func New[T any](config Config[T]) (*cache[T], error) { + + builder, err := otter.NewBuilder[string, swrEntry[T]](config.MaxSize) + if err != nil { + return nil, fault.Wrap(err, fault.WithDesc("failed to create otter builder", "")) + } + + otter, err := builder.CollectStats().Cost(func(key string, value swrEntry[T]) uint32 { + return 1 + }).WithTTL(time.Hour).Build() + if err != nil { + return nil, fault.Wrap(err, fault.WithDesc("failed to create otter cache", "")) + } + + c := &cache[T]{ + otter: otter, + fresh: config.Fresh, + stale: config.Stale, + refreshFromOrigin: config.RefreshFromOrigin, + refreshC: make(chan string, 1000), + logger: config.Logger, + resource: config.Resource, + clock: config.Clock, + } + + go c.runRefreshing() + + return c, nil + +} + +func (c cache[T]) Get(ctx context.Context, key string) (value T, hit CacheHit) { + + e, ok := c.otter.Get(key) + if !ok { + // This hack is necessary because you can not return nil as T + var t T + return t, Miss + } + + now := c.clock.Now() + + if now.Before(e.Fresh) { + + return e.Value, e.Hit + + } + if now.Before(e.Stale) { + c.refreshC <- key + + return e.Value, e.Hit + } + + c.otter.Delete(key) + + var t T + return t, Miss + +} + +func (c cache[T]) SetNull(ctx context.Context, key string) { + c.set(ctx, key) +} + +func (c cache[T]) Set(ctx context.Context, key string, value T) { + c.set(ctx, key, value) +} +func (c cache[T]) set(_ context.Context, key string, value ...T) { + now := c.clock.Now() + + e := swrEntry[T]{ + Value: value[0], + Fresh: now.Add(c.fresh), + Stale: now.Add(c.stale), + Hit: Null, + } + if len(value) > 0 { + e.Value = value[0] + e.Hit = Hit + } else { + e.Hit = Miss + } + c.otter.Set(key, e) + +} + +func (c cache[T]) Remove(ctx context.Context, key string) { + + c.otter.Delete(key) + +} + +func (c cache[T]) Dump(ctx context.Context) ([]byte, error) { + data := make(map[string]swrEntry[T]) + + c.otter.Range(func(key string, entry swrEntry[T]) bool { + data[key] = entry + return true + }) + + b, err := json.Marshal(data) + + if err != nil { + return nil, fault.Wrap(err, fault.WithDesc("failed to marshal cache data", "")) + } + return b, nil + +} + +func (c cache[T]) Restore(ctx context.Context, b []byte) error { + + data := make(map[string]swrEntry[T]) + err := json.Unmarshal(b, &data) + if err != nil { + return fmt.Errorf("failed to unmarshal cache data: %w", err) + } + now := c.clock.Now() + for key, entry := range data { + if now.Before(entry.Fresh) { + c.Set(ctx, key, entry.Value) + } else if now.Before(entry.Stale) { + c.refreshC <- key + } + // If the entry is older than, we don't restore it + } + return nil +} + +func (c cache[T]) Clear(ctx context.Context) { + c.otter.Clear() +} + +func (c cache[T]) runRefreshing() { + for { + ctx := context.Background() + identifier := <-c.refreshC + + ctx, span := tracing.Start(ctx, tracing.NewSpanName(fmt.Sprintf("cache.%s", c.resource), "refresh")) + span.SetAttributes(attribute.String("identifier", identifier)) + t, ok := c.refreshFromOrigin(ctx, identifier) + if !ok { + span.AddEvent("identifier not found in origin") + c.logger.Warn(ctx, "origin couldn't find data", slog.String("identifier", identifier)) + span.End() + continue + } + c.Set(ctx, identifier, t) + span.End() + } + +} + +func (c cache[T]) SWR(ctx context.Context, identifier string) (T, bool) { + + value, hit := c.Get(ctx, identifier) + + if hit == Hit { + return value, true + } + if hit == Null { + return value, false + } + + value, found := c.refreshFromOrigin(ctx, identifier) + if found { + c.Set(ctx, identifier, value) + return value, true + } + c.SetNull(ctx, identifier) + return value, false + +} diff --git a/go/pkg/cache/cache_test.go b/go/pkg/cache/cache_test.go new file mode 100644 index 0000000000..342bf8752c --- /dev/null +++ b/go/pkg/cache/cache_test.go @@ -0,0 +1,114 @@ +package cache_test + +import ( + "context" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/unkeyed/unkey/go/pkg/cache" + "github.com/unkeyed/unkey/go/pkg/clock" + "github.com/unkeyed/unkey/go/pkg/logging" +) + +func TestWriteRead(t *testing.T) { + + c, err := cache.New[string](cache.Config[string]{ + MaxSize: 10_000, + + Fresh: time.Minute, + Stale: time.Minute * 5, + RefreshFromOrigin: func(ctx context.Context, id string) (string, bool) { + return "hello", true + }, + Logger: logging.NewNoop(), + Resource: "test", Clock: clock.New(), + }) + require.NoError(t, err) + c.Set(context.Background(), "key", "value") + value, hit := c.Get(context.Background(), "key") + require.Equal(t, cache.Hit, hit) + require.Equal(t, "value", value) +} + +func TestEviction(t *testing.T) { + + clk := clock.NewTestClock() + c, err := cache.New[string](cache.Config[string]{ + MaxSize: 10_000, + + Fresh: time.Second, + Stale: time.Second, + RefreshFromOrigin: func(ctx context.Context, id string) (string, bool) { + return "hello", true + }, + Logger: logging.NewNoop(), + Resource: "test", + Clock: clk, + }) + require.NoError(t, err) + + c.Set(context.Background(), "key", "value") + clk.Tick(2 * time.Second) + _, hit := c.Get(context.Background(), "key") + require.Equal(t, cache.Miss, hit) +} + +func TestRefresh(t *testing.T) { + + clk := clock.NewTestClock() + + // count how many times we refreshed from origin + refreshedFromOrigin := atomic.Int32{} + + c, err := cache.New[string](cache.Config[string]{ + MaxSize: 10_000, + + Fresh: time.Second * 2, + Stale: time.Minute * 5, + RefreshFromOrigin: func(ctx context.Context, id string) (string, bool) { + refreshedFromOrigin.Add(1) + + t.Log("called", id, clk.Now()) + return "hello", true + }, + Logger: logging.NewNoop(), + Resource: "test", + Clock: clk, + }) + require.NoError(t, err) + + c.Set(context.Background(), "key", "value") + clk.Tick(time.Second) + + for i := 0; i < 10; i++ { + _, hit := c.Get(context.Background(), "key") + require.Equal(t, cache.Hit, hit) + clk.Tick(time.Second) + } + require.LessOrEqual(t, refreshedFromOrigin.Load(), int32(5)) + +} + +func TestNull(t *testing.T) { + t.Skip() + + c, err := cache.New[string](cache.Config[string]{ + MaxSize: 10_000, + Fresh: time.Second * 1, + Stale: time.Minute * 5, + Logger: logging.NewNoop(), + RefreshFromOrigin: nil, + Resource: "test", + Clock: clock.New(), + }) + require.NoError(t, err) + + c.SetNull(context.Background(), "key") + + _, hit := c.Get(context.Background(), "key") + require.Equal(t, cache.Null, hit) + +} diff --git a/go/pkg/cache/entry.go b/go/pkg/cache/entry.go new file mode 100644 index 0000000000..2577bebe9e --- /dev/null +++ b/go/pkg/cache/entry.go @@ -0,0 +1,16 @@ +package cache + +import ( + "time" +) + +type swrEntry[T any] struct { + Value T `json:"value"` + + Hit CacheHit `json:"hit"` + // Before this time the entry is considered fresh and vaid + Fresh time.Time `json:"fresh"` + // Before this time, the entry should be revalidated + // After this time, the entry must be discarded + Stale time.Time `json:"stale"` +} diff --git a/go/pkg/cache/interface.go b/go/pkg/cache/interface.go new file mode 100644 index 0000000000..14474732d1 --- /dev/null +++ b/go/pkg/cache/interface.go @@ -0,0 +1,43 @@ +package cache + +import ( + "context" +) + +type Cache[T any] interface { + // Get returns the value for the given key. + // If the key is not found, found will be false. + Get(ctx context.Context, key string) (value T, hit CacheHit) + + // Sets the value for the given key. + Set(ctx context.Context, key string, value T) + + // Sets the given key to null, indicating that the value does not exist in the origin. + SetNull(ctx context.Context, key string) + + // Removes the key from the cache. + Remove(ctx context.Context, key string) + + SWR(ctx context.Context, key string) (value T, found bool) + + // Dump returns a serialized representation of the cache. + Dump(ctx context.Context) ([]byte, error) + + // Restore restores the cache from a serialized representation. + Restore(ctx context.Context, data []byte) error + + // Clear removes all entries from the cache. + Clear(ctx context.Context) +} + +type CacheHit int + +const ( + Null CacheHit = iota + // The entry was in the cache and can be used + Hit + // The entry was not in the cache + Miss + // The entry did not exist in the origin + +) diff --git a/go/pkg/cache/middleware.go b/go/pkg/cache/middleware.go new file mode 100644 index 0000000000..971d7b1c1d --- /dev/null +++ b/go/pkg/cache/middleware.go @@ -0,0 +1,3 @@ +package cache + +type Middleware[T any] func(Cache[T]) Cache[T] diff --git a/go/pkg/cache/middleware/metrics.go b/go/pkg/cache/middleware/metrics.go new file mode 100644 index 0000000000..6440c778af --- /dev/null +++ b/go/pkg/cache/middleware/metrics.go @@ -0,0 +1,68 @@ +package middleware + +import ( + "context" + "time" + + "github.com/unkeyed/unkey/apps/agent/pkg/cache" + "github.com/unkeyed/unkey/apps/agent/pkg/metrics" + "github.com/unkeyed/unkey/apps/agent/pkg/prometheus" +) + +type metricsMiddleware[T any] struct { + next cache.Cache[T] + metrics metrics.Metrics + resource string + tier string +} + +func WithMetrics[T any](c cache.Cache[T], m metrics.Metrics, resource string, tier string) cache.Cache[T] { + return &metricsMiddleware[T]{next: c, metrics: m, resource: resource, tier: tier} +} + +func (mw *metricsMiddleware[T]) Get(ctx context.Context, key string) (T, cache.CacheHit) { + start := time.Now() + value, hit := mw.next.Get(ctx, key) + + labels := map[string]string{ + "key": key, + "resource": mw.resource, + "tier": mw.tier, + } + + if hit == cache.Miss { + prometheus.CacheMisses.With(labels).Inc() + } else { + prometheus.CacheHits.With(labels).Inc() + } + prometheus.CacheLatency.With(labels).Observe(time.Since(start).Seconds()) + + return value, hit +} +func (mw *metricsMiddleware[T]) Set(ctx context.Context, key string, value T) { + mw.next.Set(ctx, key, value) + +} +func (mw *metricsMiddleware[T]) SetNull(ctx context.Context, key string) { + mw.next.SetNull(ctx, key) + +} +func (mw *metricsMiddleware[T]) Remove(ctx context.Context, key string) { + + mw.next.Remove(ctx, key) + +} + +func (mw *metricsMiddleware[T]) Dump(ctx context.Context) ([]byte, error) { + // nolint:wrapcheck + return mw.next.Dump(ctx) +} + +func (mw *metricsMiddleware[T]) Restore(ctx context.Context, data []byte) error { + // nolint:wrapcheck + return mw.next.Restore(ctx, data) +} + +func (mw *metricsMiddleware[T]) Clear(ctx context.Context) { + mw.next.Clear(ctx) +} diff --git a/go/pkg/cache/middleware/tracing.go b/go/pkg/cache/middleware/tracing.go new file mode 100644 index 0000000000..d6cbe6be73 --- /dev/null +++ b/go/pkg/cache/middleware/tracing.go @@ -0,0 +1,95 @@ +package middleware + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/cache" + "github.com/unkeyed/unkey/go/pkg/tracing" + "go.opentelemetry.io/otel/attribute" +) + +type tracingMiddleware[T any] struct { + next cache.Cache[T] +} + +func WithTracing[T any](c cache.Cache[T]) cache.Cache[T] { + return &tracingMiddleware[T]{next: c} +} + +func (mw *tracingMiddleware[T]) Get(ctx context.Context, key string) (T, cache.CacheHit) { + ctx, span := tracing.Start(ctx, "cache.Get") + defer span.End() + span.SetAttributes(attribute.String("key", key)) + + value, hit := mw.next.Get(ctx, key) + span.SetAttributes( + attribute.Bool("hit", hit != cache.Miss), + ) + return value, hit +} +func (mw *tracingMiddleware[T]) Set(ctx context.Context, key string, value T) { + ctx, span := tracing.Start(ctx, "cache.Set") + defer span.End() + span.SetAttributes(attribute.String("key", key)) + + mw.next.Set(ctx, key, value) + +} +func (mw *tracingMiddleware[T]) SetNull(ctx context.Context, key string) { + ctx, span := tracing.Start(ctx, "cache.SetNull") + defer span.End() + + span.SetAttributes(attribute.String("key", key)) + mw.next.SetNull(ctx, key) + +} +func (mw *tracingMiddleware[T]) Remove(ctx context.Context, key string) { + ctx, span := tracing.Start(ctx, "cache.Remove") + defer span.End() + span.SetAttributes(attribute.String("key", key)) + + mw.next.Remove(ctx, key) + +} + +func (mw *tracingMiddleware[T]) Dump(ctx context.Context) ([]byte, error) { + ctx, span := tracing.Start(ctx, "cache.Dump") + defer span.End() + + b, err := mw.next.Dump(ctx) + if err != nil { + tracing.RecordError(span, err) + } + // nolint:wrapcheck + return b, err +} + +func (mw *tracingMiddleware[T]) Restore(ctx context.Context, data []byte) error { + ctx, span := tracing.Start(ctx, "cache.Restore") + defer span.End() + + err := mw.next.Restore(ctx, data) + if err != nil { + tracing.RecordError(span, err) + } + // nolint:wrapcheck + return err +} + +func (mw *tracingMiddleware[T]) Clear(ctx context.Context) { + ctx, span := tracing.Start(ctx, "cache.Clear") + defer span.End() + + mw.next.Clear(ctx) +} + +func (mw *tracingMiddleware[T]) SWR(ctx context.Context, key string) (T, bool) { + ctx, span := tracing.Start(ctx, "cache.SWR") + defer span.End() + span.SetAttributes(attribute.String("key", key)) + + value, found := mw.next.SWR(ctx, key) + span.SetAttributes(attribute.Bool("found", found)) + return value, found + +} diff --git a/go/pkg/cache/noop.go b/go/pkg/cache/noop.go new file mode 100644 index 0000000000..e5b8d18cb2 --- /dev/null +++ b/go/pkg/cache/noop.go @@ -0,0 +1,32 @@ +package cache + +import ( + "context" +) + +type noopCache[T any] struct{} + +func (c *noopCache[T]) Get(ctx context.Context, key string) (value T, hit CacheHit) { + var t T + return t, Miss +} +func (c *noopCache[T]) Set(ctx context.Context, key string, value T) {} +func (c *noopCache[T]) SetNull(ctx context.Context, key string) {} + +func (c *noopCache[T]) Remove(ctx context.Context, key string) {} + +func (c *noopCache[T]) Dump(ctx context.Context) ([]byte, error) { + return []byte{}, nil +} +func (c *noopCache[T]) Restore(ctx context.Context, data []byte) error { + return nil +} +func (c *noopCache[T]) Clear(ctx context.Context) {} +func (c *noopCache[T]) SWR(ctx context.Context, key string) (T, bool) { + var t T + return t, false +} + +func NewNoopCache[T any]() Cache[T] { + return &noopCache[T]{} +} diff --git a/go/pkg/cache/util.go b/go/pkg/cache/util.go new file mode 100644 index 0000000000..3f703d6755 --- /dev/null +++ b/go/pkg/cache/util.go @@ -0,0 +1,33 @@ +package cache + +import ( + "context" +) + +// withCache builds a pullthrough cache function to wrap a database call. +// Example: +// api, found, err := withCache(s.apiCache, s.db.FindApiByKeyAuthId)(ctx, key.KeyAuthId) +func WithCache[T any](c Cache[T], loadFromDatabase func(ctx context.Context, identifier string) (T, bool, error)) func(ctx context.Context, identifier string) (T, bool, error) { + return func(ctx context.Context, identifier string) (T, bool, error) { + value, hit := c.Get(ctx, identifier) + + if hit == Hit { + return value, true, nil + } + if hit == Null { + return value, false, nil + } + + value, found, err := loadFromDatabase(ctx, identifier) + if err != nil { + return value, false, err + } + if found { + c.Set(ctx, identifier, value) + return value, true, nil + } else { + c.SetNull(ctx, identifier) + return value, false, nil + } + } +} diff --git a/go/pkg/clickhouse/flush.go b/go/pkg/clickhouse/flush.go index 12778c1417..790ce5f923 100644 --- a/go/pkg/clickhouse/flush.go +++ b/go/pkg/clickhouse/flush.go @@ -5,24 +5,23 @@ import ( "fmt" ch "github.com/ClickHouse/clickhouse-go/v2" - "github.com/Southclaws/fault" - "github.com/Southclaws/fault/fmsg" + "github.com/unkeyed/unkey/go/pkg/fault" ) func flush[T any](ctx context.Context, conn ch.Conn, table string, rows []T) error { batch, err := conn.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s", table)) if err != nil { - return fault.Wrap(err, fmsg.With("preparing batch failed")) + return fault.Wrap(err, fault.WithDesc("preparing batch failed", "")) } for _, row := range rows { err = batch.AppendStruct(&row) if err != nil { - return fault.Wrap(err, fmsg.With("appending struct to batch failed")) + return fault.Wrap(err, fault.WithDesc("appending struct to batch failed", "")) } } err = batch.Send() if err != nil { - return fault.Wrap(err, fmsg.With("committing batch failed")) + return fault.Wrap(err, fault.WithDesc("committing batch failed", "")) } return nil } diff --git a/go/pkg/clock/real_clock_test.go b/go/pkg/clock/real_clock_test.go new file mode 100644 index 0000000000..9fd99733d0 --- /dev/null +++ b/go/pkg/clock/real_clock_test.go @@ -0,0 +1,18 @@ +package clock + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRealClock(t *testing.T) { + clock := New() + before := time.Now() + now := clock.Now() + after := time.Now() + + require.False(t, now.Before(before), "time should not be before test start") + require.False(t, now.After(after), "time should not be after test end") +} diff --git a/go/pkg/clock/test_clock.go b/go/pkg/clock/test_clock.go index 32b0fd2cf4..6467c67599 100644 --- a/go/pkg/clock/test_clock.go +++ b/go/pkg/clock/test_clock.go @@ -1,8 +1,12 @@ package clock -import "time" +import ( + "sync" + "time" +) type TestClock struct { + mu sync.RWMutex now time.Time } @@ -10,24 +14,30 @@ func NewTestClock(now ...time.Time) *TestClock { if len(now) == 0 { now = append(now, time.Now()) } - return &TestClock{now: now[0]} + return &TestClock{mu: sync.RWMutex{}, now: now[0]} } // nolint:exhaustruct var _ Clock = &TestClock{} func (c *TestClock) Now() time.Time { + c.mu.RLock() + defer c.mu.RUnlock() return c.now } // Tick advances the clock by the given duration and returns the new time. func (c *TestClock) Tick(d time.Duration) time.Time { + c.mu.Lock() + defer c.mu.Unlock() c.now = c.now.Add(d) return c.now } // Set sets the clock to the given time and returns the new time. func (c *TestClock) Set(t time.Time) time.Time { + c.mu.Lock() + defer c.mu.Unlock() c.now = t return c.now } diff --git a/go/pkg/clock/test_clock_test.go b/go/pkg/clock/test_clock_test.go new file mode 100644 index 0000000000..18d4fac3f0 --- /dev/null +++ b/go/pkg/clock/test_clock_test.go @@ -0,0 +1,98 @@ +package clock + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTestClock(t *testing.T) { + t.Run("NewTestClock with no args uses current time", func(t *testing.T) { + before := time.Now() + clock := NewTestClock() + after := time.Now() + + now := clock.Now() + require.False(t, now.Before(before), "time should not be before test start") + require.False(t, now.After(after), "time should not be after test end") + }) + + t.Run("NewTestClock with specific time", func(t *testing.T) { + specificTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + clock := NewTestClock(specificTime) + + require.True(t, clock.Now().Equal(specificTime)) + }) + + t.Run("Tick advances time correctly", func(t *testing.T) { + startTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + clock := NewTestClock(startTime) + + duration := 5 * time.Minute + newTime := clock.Tick(duration) + expected := startTime.Add(duration) + + require.True(t, newTime.Equal(expected)) + require.True(t, clock.Now().Equal(expected)) + }) + + t.Run("Set changes time correctly", func(t *testing.T) { + clock := NewTestClock() + newTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + + returnedTime := clock.Set(newTime) + + require.True(t, returnedTime.Equal(newTime)) + require.True(t, clock.Now().Equal(newTime)) + }) + + t.Run("Multiple operations in sequence", func(t *testing.T) { + startTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + clock := NewTestClock(startTime) + + // Tick forward + clock.Tick(time.Hour) + expected := startTime.Add(time.Hour) + require.True(t, clock.Now().Equal(expected)) + + // Set to new time + newTime := time.Date(2023, 2, 1, 12, 0, 0, 0, time.UTC) + clock.Set(newTime) + require.True(t, clock.Now().Equal(newTime)) + + // Tick again + clock.Tick(30 * time.Minute) + expected = newTime.Add(30 * time.Minute) + require.True(t, clock.Now().Equal(expected)) + }) +} + +func TestTestClockWithDifferentTimeZones(t *testing.T) { + nyc, err := time.LoadLocation("America/New_York") + require.NoError(t, err) + + startTime := time.Date(2023, 1, 1, 12, 0, 0, 0, nyc) + clock := NewTestClock(startTime) + + require.True(t, clock.Now().Equal(startTime)) + require.Equal(t, nyc, clock.Now().Location()) +} + +func TestTestClockWithNegativeTick(t *testing.T) { + startTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) + clock := NewTestClock(startTime) + + newTime := clock.Tick(-1 * time.Hour) + expected := startTime.Add(-1 * time.Hour) + + require.True(t, newTime.Equal(expected)) +} + +func TestClockInterface(t *testing.T) { + var realClock Clock = &RealClock{} + var testClock Clock = &TestClock{} + + require.Implements(t, (*Clock)(nil), realClock) + require.Implements(t, (*Clock)(nil), testClock) +} diff --git a/go/pkg/database/database.go b/go/pkg/database/database.go index 5eb089154f..08530d7ee0 100644 --- a/go/pkg/database/database.go +++ b/go/pkg/database/database.go @@ -5,16 +5,95 @@ import ( "github.com/unkeyed/unkey/go/pkg/database/gen" "github.com/unkeyed/unkey/go/pkg/fault" + "github.com/unkeyed/unkey/go/pkg/logging" ) -type Database = gen.Querier +type Config struct { + // The primary DSN for your database. This must support both reads and writes. + PrimaryDSN string -func New(dsn string) (Database, error) { - db, err := sql.Open("mysql", dsn) + // The readonly replica will be used for most read queries. + // If omitted, the primary is used. + ReadOnlyDSN string + + Logger logging.Logger +} + +type replica struct { + db *sql.DB + query *gen.Queries +} + +type database struct { + writeReplica *replica + readReplica *replica + logger logging.Logger +} + +func New(config Config, middlewares ...Middleware) (Database, error) { + + write, err := sql.Open("mysql", config.PrimaryDSN) if err != nil { - return nil, fault.Wrap(err, fault.WithDesc("unable to open mysql connection", "")) + return nil, fault.Wrap(err, fault.WithDesc("cannot open primary replica", "")) + } + + writeReplica := &replica{ + db: write, + query: gen.New(write), + } + readReplica := &replica{ + db: write, + query: gen.New(write), + } + if config.ReadOnlyDSN != "" { + read, err := sql.Open("mysql", config.ReadOnlyDSN) + if err != nil { + return nil, fault.Wrap(err, fault.WithDesc("cannot open read replica", "")) + } + readReplica = &replica{ + db: read, + query: gen.New(read), + } + + } + + var wrapped Database = &database{ + writeReplica: writeReplica, + readReplica: readReplica, + logger: config.Logger, + } + + for _, mw := range middlewares { + wrapped = mw(wrapped) + } + + return wrapped, nil + +} + +func (d *database) write() *gen.Queries { + return d.writeReplica.query +} + +func (d *database) read() *gen.Queries { + if d.readReplica != nil { + return d.readReplica.query } + return d.writeReplica.query +} - return gen.New(db), nil +func (d *database) Close() error { + writeCloseErr := d.writeReplica.db.Close() + + if d.readReplica != nil { + readCloseErr := d.readReplica.db.Close() + if readCloseErr != nil { + return fault.Wrap(readCloseErr) + } + } + if writeCloseErr != nil { + return fault.Wrap(writeCloseErr) + } + return nil } diff --git a/go/pkg/database/gen/key_find_by_hash.sql.go b/go/pkg/database/gen/key_find_by_hash.sql.go new file mode 100644 index 0000000000..caaf243587 --- /dev/null +++ b/go/pkg/database/gen/key_find_by_hash.sql.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: key_find_by_hash.sql + +package gen + +import ( + "context" +) + +const findKeyByID = `-- name: FindKeyByID :one +SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM ` + "`" + `keys` + "`" + ` +WHERE id = ? +` + +// FindKeyByID +// +// SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM `keys` +// WHERE id = ? +func (q *Queries) FindKeyByID(ctx context.Context, id string) (Key, error) { + row := q.db.QueryRowContext(ctx, findKeyByID, id) + var i Key + err := row.Scan( + &i.ID, + &i.KeyAuthID, + &i.Hash, + &i.Start, + &i.WorkspaceID, + &i.ForWorkspaceID, + &i.Name, + &i.OwnerID, + &i.IdentityID, + &i.Meta, + &i.CreatedAt, + &i.Expires, + &i.CreatedAtM, + &i.UpdatedAtM, + &i.DeletedAtM, + &i.DeletedAt, + &i.RefillDay, + &i.RefillAmount, + &i.LastRefillAt, + &i.Enabled, + &i.RemainingRequests, + &i.RatelimitAsync, + &i.RatelimitLimit, + &i.RatelimitDuration, + &i.Environment, + ) + return i, err +} diff --git a/go/pkg/database/gen/key_find_by_id.sql.go b/go/pkg/database/gen/key_find_by_id.sql.go new file mode 100644 index 0000000000..918c8a8bc5 --- /dev/null +++ b/go/pkg/database/gen/key_find_by_id.sql.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: key_find_by_id.sql + +package gen + +import ( + "context" +) + +const findRatelimitOverrideByIdentifier = `-- name: FindRatelimitOverrideByIdentifier :one +SELECT id, workspace_id, namespace_id, identifier, ` + "`" + `limit` + "`" + `, duration, async, sharding, created_at, updated_at, deleted_at FROM ` + "`" + `ratelimit_overrides` + "`" + ` +WHERE identifier = ? +` + +// FindRatelimitOverrideByIdentifier +// +// SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at, updated_at, deleted_at FROM `ratelimit_overrides` +// WHERE identifier = ? +func (q *Queries) FindRatelimitOverrideByIdentifier(ctx context.Context, identifier string) (RatelimitOverride, error) { + row := q.db.QueryRowContext(ctx, findRatelimitOverrideByIdentifier, identifier) + var i RatelimitOverride + err := row.Scan( + &i.ID, + &i.WorkspaceID, + &i.NamespaceID, + &i.Identifier, + &i.Limit, + &i.Duration, + &i.Async, + &i.Sharding, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/go/pkg/database/gen/querier.go b/go/pkg/database/gen/querier.go index c3c6227a81..c99f2f89c4 100644 --- a/go/pkg/database/gen/querier.go +++ b/go/pkg/database/gen/querier.go @@ -6,14 +6,60 @@ package gen import ( "context" + "database/sql" ) type Querier interface { - //GetKeyByHash + //DeleteRatelimitNamespace + // + // UPDATE `ratelimit_namespaces` + // SET deleted_at = NOW() + // WHERE id = ? + DeleteRatelimitNamespace(ctx context.Context, id string) (sql.Result, error) + //DeleteRatelimitOverride + // + // UPDATE `ratelimit_overrides` + // SET + // deleted_at = NOW() + // WHERE id = ? + DeleteRatelimitOverride(ctx context.Context, id string) (sql.Result, error) + //FindKeyByHash // // SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM `keys` // WHERE hash = ? - GetKeyByHash(ctx context.Context, hash string) (Key, error) + FindKeyByHash(ctx context.Context, hash string) (Key, error) + //FindKeyByID + // + // SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM `keys` + // WHERE id = ? + FindKeyByID(ctx context.Context, id string) (Key, error) + //FindRatelimitNamespaceByID + // + // SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM `ratelimit_namespaces` + // WHERE id = ? + FindRatelimitNamespaceByID(ctx context.Context, id string) (RatelimitNamespace, error) + //FindRatelimitNamespaceByName + // + // SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM `ratelimit_namespaces` + // WHERE name = ? + // AND workspace_id = ? + FindRatelimitNamespaceByName(ctx context.Context, arg FindRatelimitNamespaceByNameParams) (RatelimitNamespace, error) + //FindRatelimitOverrideByIdentifier + // + // SELECT id, workspace_id, namespace_id, identifier, `limit`, duration, async, sharding, created_at, updated_at, deleted_at FROM `ratelimit_overrides` + // WHERE identifier = ? + FindRatelimitOverrideByIdentifier(ctx context.Context, identifier string) (RatelimitOverride, error) + //FindWorkspaceByID + // + // SELECT id, tenant_id, name, created_at, deleted_at, plan, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, subscriptions, enabled, delete_protection FROM `workspaces` + // WHERE id = ? + FindWorkspaceByID(ctx context.Context, id string) (Workspace, error) + //HardDeleteWorkspace + // + // DELETE FROM `workspaces` + // WHERE id = ? + // AND delete_protection = false + HardDeleteWorkspace(ctx context.Context, id string) (sql.Result, error) //InsertOverride // // INSERT INTO @@ -39,6 +85,119 @@ type Querier interface { // now() // ) InsertOverride(ctx context.Context, arg InsertOverrideParams) error + //InsertWorkspace + // + // INSERT INTO `workspaces` ( + // id, + // tenant_id, + // name, + // created_at, + // plan, + // beta_features, + // features, + // enabled, + // delete_protection + // ) + // VALUES ( + // ?, + // ?, + // ?, + // NOW(), + // 'free', + // '{}', + // '{}', + // true, + // true + // ) + InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) error + //SoftDeleteWorkspace + // + // UPDATE `workspaces` + // SET deleted_at = NOW() + // WHERE id = ? + // AND delete_protection = false + SoftDeleteWorkspace(ctx context.Context, id string) (sql.Result, error) + //UpdateRatelimitOverride + // + // UPDATE `ratelimit_overrides` + // SET + // `limit` = ?, + // duration = ?, + // async = ?, + // updated_at = NOW() + // WHERE id = ? + UpdateRatelimitOverride(ctx context.Context, arg UpdateRatelimitOverrideParams) (sql.Result, error) + //UpdateWorkspaceEnabled + // + // UPDATE `workspaces` + // SET enabled = ? + // WHERE id = ? + UpdateWorkspaceEnabled(ctx context.Context, arg UpdateWorkspaceEnabledParams) (sql.Result, error) + //UpdateWorkspacePlan + // + // UPDATE `workspaces` + // SET plan = ? + // WHERE id = ? + UpdateWorkspacePlan(ctx context.Context, arg UpdateWorkspacePlanParams) (sql.Result, error) + //VerifyKey + // + // WITH direct_permissions AS ( + // SELECT kp.key_id, p.name as permission_name + // FROM keys_permissions kp + // JOIN permissions p ON kp.permission_id = p.id + // ), + // role_permissions AS ( + // SELECT kr.key_id, p.name as permission_name + // FROM keys_roles kr + // JOIN roles_permissions rp ON kr.role_id = rp.role_id + // JOIN permissions p ON rp.permission_id = p.id + // ), + // all_permissions AS ( + // SELECT key_id, permission_name FROM direct_permissions + // UNION + // SELECT key_id, permission_name FROM role_permissions + // ), + // all_ratelimits AS ( + // SELECT + // key_id as target_id, + // 'key' as target_type, + // name, + // `limit`, + // duration + // FROM ratelimits + // WHERE key_id IS NOT NULL + // UNION + // SELECT + // identity_id as target_id, + // 'identity' as target_type, + // name, + // `limit`, + // duration + // FROM ratelimits + // WHERE identity_id IS NOT NULL + // ) + // SELECT + // k.id, k.key_auth_id, k.hash, k.start, k.workspace_id, k.for_workspace_id, k.name, k.owner_id, k.identity_id, k.meta, k.created_at, k.expires, k.created_at_m, k.updated_at_m, k.deleted_at_m, k.deleted_at, k.refill_day, k.refill_amount, k.last_refill_at, k.enabled, k.remaining_requests, k.ratelimit_async, k.ratelimit_limit, k.ratelimit_duration, k.environment, + // i.id, i.external_id, i.workspace_id, i.environment, i.created_at, i.updated_at, i.meta, + // JSON_ARRAYAGG( + // JSON_OBJECT( + // 'target_type', rl.target_type, + // 'name', rl.name, + // 'limit', rl.limit, + // 'duration', rl.duration + // ) + // ) as ratelimits, + // GROUP_CONCAT(DISTINCT perms.permission_name) as permissions + // FROM `keys` k + // LEFT JOIN identities i ON k.identity_id = i.id + // LEFT JOIN all_permissions perms ON k.id = perms.key_id + // LEFT JOIN all_ratelimits rl ON ( + // (rl.target_type = 'key' AND rl.target_id = k.id) OR + // (rl.target_type = 'identity' AND rl.target_id = k.identity_id) + // ) + // WHERE k.hash = ? + // GROUP BY k.id + VerifyKey(ctx context.Context, hash string) (VerifyKeyRow, error) } var _ Querier = (*Queries)(nil) diff --git a/go/pkg/database/gen/ratelimit_namespace_delete.sql.go b/go/pkg/database/gen/ratelimit_namespace_delete.sql.go new file mode 100644 index 0000000000..0da1e61184 --- /dev/null +++ b/go/pkg/database/gen/ratelimit_namespace_delete.sql.go @@ -0,0 +1,26 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_namespace_delete.sql + +package gen + +import ( + "context" + "database/sql" +) + +const deleteRatelimitNamespace = `-- name: DeleteRatelimitNamespace :execresult +UPDATE ` + "`" + `ratelimit_namespaces` + "`" + ` +SET deleted_at = NOW() +WHERE id = ? +` + +// DeleteRatelimitNamespace +// +// UPDATE `ratelimit_namespaces` +// SET deleted_at = NOW() +// WHERE id = ? +func (q *Queries) DeleteRatelimitNamespace(ctx context.Context, id string) (sql.Result, error) { + return q.db.ExecContext(ctx, deleteRatelimitNamespace, id) +} diff --git a/go/pkg/database/gen/ratelimit_namespace_find_by_id.sql.go b/go/pkg/database/gen/ratelimit_namespace_find_by_id.sql.go new file mode 100644 index 0000000000..fa9776fc4e --- /dev/null +++ b/go/pkg/database/gen/ratelimit_namespace_find_by_id.sql.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_namespace_find_by_id.sql + +package gen + +import ( + "context" +) + +const findRatelimitNamespaceByID = `-- name: FindRatelimitNamespaceByID :one +SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM ` + "`" + `ratelimit_namespaces` + "`" + ` +WHERE id = ? +` + +// FindRatelimitNamespaceByID +// +// SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM `ratelimit_namespaces` +// WHERE id = ? +func (q *Queries) FindRatelimitNamespaceByID(ctx context.Context, id string) (RatelimitNamespace, error) { + row := q.db.QueryRowContext(ctx, findRatelimitNamespaceByID, id) + var i RatelimitNamespace + err := row.Scan( + &i.ID, + &i.WorkspaceID, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/go/pkg/database/gen/ratelimit_namespace_find_by_name.sql.go b/go/pkg/database/gen/ratelimit_namespace_find_by_name.sql.go new file mode 100644 index 0000000000..0c18df92ae --- /dev/null +++ b/go/pkg/database/gen/ratelimit_namespace_find_by_name.sql.go @@ -0,0 +1,40 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_namespace_find_by_name.sql + +package gen + +import ( + "context" +) + +const findRatelimitNamespaceByName = `-- name: FindRatelimitNamespaceByName :one +SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM ` + "`" + `ratelimit_namespaces` + "`" + ` +WHERE name = ? +AND workspace_id = ? +` + +type FindRatelimitNamespaceByNameParams struct { + Name string `db:"name"` + WorkspaceID string `db:"workspace_id"` +} + +// FindRatelimitNamespaceByName +// +// SELECT id, workspace_id, name, created_at, updated_at, deleted_at FROM `ratelimit_namespaces` +// WHERE name = ? +// AND workspace_id = ? +func (q *Queries) FindRatelimitNamespaceByName(ctx context.Context, arg FindRatelimitNamespaceByNameParams) (RatelimitNamespace, error) { + row := q.db.QueryRowContext(ctx, findRatelimitNamespaceByName, arg.Name, arg.WorkspaceID) + var i RatelimitNamespace + err := row.Scan( + &i.ID, + &i.WorkspaceID, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/go/pkg/database/gen/ratelimit_namespace_insert.sql.go b/go/pkg/database/gen/ratelimit_namespace_insert.sql.go new file mode 100644 index 0000000000..a053a29763 --- /dev/null +++ b/go/pkg/database/gen/ratelimit_namespace_insert.sql.go @@ -0,0 +1,50 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_namespace_insert.sql + +package gen + +import ( + "context" +) + +const insertRatelimitNamespace = `-- name: InsertRatelimitNamespace :exec +INSERT INTO ` + "`" + `ratelimit_namespaces` + "`" + ` ( + id, + workspace_id, + name, + created_at +) +VALUES ( + ?, + ?, + ?, + NOW() +) +` + +type InsertRatelimitNamespaceParams struct { + ID string `db:"id"` + WorkspaceID string `db:"workspace_id"` + Name string `db:"name"` +} + +// InsertRatelimitNamespace +// +// INSERT INTO `ratelimit_namespaces` ( +// id, +// workspace_id, +// name, +// created_at +// ) +// VALUES ( +// ?, +// ?, +// ?, +// NOW() +// ) +func (q *Queries) InsertRatelimitNamespace(ctx context.Context, arg InsertRatelimitNamespaceParams) error { + _, err := q.db.ExecContext(ctx, insertRatelimitNamespace, arg.ID, arg.WorkspaceID, arg.Name) + return err +} diff --git a/go/pkg/database/gen/ratelimit_override_delete.sql.go b/go/pkg/database/gen/ratelimit_override_delete.sql.go new file mode 100644 index 0000000000..d8f002389d --- /dev/null +++ b/go/pkg/database/gen/ratelimit_override_delete.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_override_delete.sql + +package gen + +import ( + "context" + "database/sql" +) + +const deleteRatelimitOverride = `-- name: DeleteRatelimitOverride :execresult +UPDATE ` + "`" + `ratelimit_overrides` + "`" + ` +SET + deleted_at = NOW() +WHERE id = ? +` + +// DeleteRatelimitOverride +// +// UPDATE `ratelimit_overrides` +// SET +// deleted_at = NOW() +// WHERE id = ? +func (q *Queries) DeleteRatelimitOverride(ctx context.Context, id string) (sql.Result, error) { + return q.db.ExecContext(ctx, deleteRatelimitOverride, id) +} diff --git a/go/pkg/database/gen/get_key_by_hash.sql.go b/go/pkg/database/gen/ratelimit_override_find_by_identifier.sql.go similarity index 83% rename from go/pkg/database/gen/get_key_by_hash.sql.go rename to go/pkg/database/gen/ratelimit_override_find_by_identifier.sql.go index 98a19a8a2b..650bf73382 100644 --- a/go/pkg/database/gen/get_key_by_hash.sql.go +++ b/go/pkg/database/gen/ratelimit_override_find_by_identifier.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.27.0 -// source: get_key_by_hash.sql +// source: ratelimit_override_find_by_identifier.sql package gen @@ -9,17 +9,17 @@ import ( "context" ) -const getKeyByHash = `-- name: GetKeyByHash :one +const findKeyByHash = `-- name: FindKeyByHash :one SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM ` + "`" + `keys` + "`" + ` WHERE hash = ? ` -// GetKeyByHash +// FindKeyByHash // // SELECT id, key_auth_id, hash, start, workspace_id, for_workspace_id, name, owner_id, identity_id, meta, created_at, expires, created_at_m, updated_at_m, deleted_at_m, deleted_at, refill_day, refill_amount, last_refill_at, enabled, remaining_requests, ratelimit_async, ratelimit_limit, ratelimit_duration, environment FROM `keys` // WHERE hash = ? -func (q *Queries) GetKeyByHash(ctx context.Context, hash string) (Key, error) { - row := q.db.QueryRowContext(ctx, getKeyByHash, hash) +func (q *Queries) FindKeyByHash(ctx context.Context, hash string) (Key, error) { + row := q.db.QueryRowContext(ctx, findKeyByHash, hash) var i Key err := row.Scan( &i.ID, diff --git a/go/pkg/database/gen/insert_override.sql.go b/go/pkg/database/gen/ratelimit_override_insert.sql.go similarity index 97% rename from go/pkg/database/gen/insert_override.sql.go rename to go/pkg/database/gen/ratelimit_override_insert.sql.go index 7a87fb8092..49d9277711 100644 --- a/go/pkg/database/gen/insert_override.sql.go +++ b/go/pkg/database/gen/ratelimit_override_insert.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.27.0 -// source: insert_override.sql +// source: ratelimit_override_insert.sql package gen diff --git a/go/pkg/database/gen/ratelimit_override_update.sql.go b/go/pkg/database/gen/ratelimit_override_update.sql.go new file mode 100644 index 0000000000..381eff64be --- /dev/null +++ b/go/pkg/database/gen/ratelimit_override_update.sql.go @@ -0,0 +1,46 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: ratelimit_override_update.sql + +package gen + +import ( + "context" + "database/sql" +) + +const updateRatelimitOverride = `-- name: UpdateRatelimitOverride :execresult +UPDATE ` + "`" + `ratelimit_overrides` + "`" + ` +SET + ` + "`" + `limit` + "`" + ` = ?, + duration = ?, + async = ?, + updated_at = NOW() +WHERE id = ? +` + +type UpdateRatelimitOverrideParams struct { + Windowlimit int32 `db:"windowlimit"` + Duration int32 `db:"duration"` + Async sql.NullBool `db:"async"` + ID string `db:"id"` +} + +// UpdateRatelimitOverride +// +// UPDATE `ratelimit_overrides` +// SET +// `limit` = ?, +// duration = ?, +// async = ?, +// updated_at = NOW() +// WHERE id = ? +func (q *Queries) UpdateRatelimitOverride(ctx context.Context, arg UpdateRatelimitOverrideParams) (sql.Result, error) { + return q.db.ExecContext(ctx, updateRatelimitOverride, + arg.Windowlimit, + arg.Duration, + arg.Async, + arg.ID, + ) +} diff --git a/go/pkg/database/gen/workspace_find_by_id.sql.go b/go/pkg/database/gen/workspace_find_by_id.sql.go new file mode 100644 index 0000000000..1c03fc5711 --- /dev/null +++ b/go/pkg/database/gen/workspace_find_by_id.sql.go @@ -0,0 +1,44 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_find_by_id.sql + +package gen + +import ( + "context" +) + +const findWorkspaceByID = `-- name: FindWorkspaceByID :one +SELECT id, tenant_id, name, created_at, deleted_at, plan, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, subscriptions, enabled, delete_protection FROM ` + "`" + `workspaces` + "`" + ` +WHERE id = ? +` + +// FindWorkspaceByID +// +// SELECT id, tenant_id, name, created_at, deleted_at, plan, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, subscriptions, enabled, delete_protection FROM `workspaces` +// WHERE id = ? +func (q *Queries) FindWorkspaceByID(ctx context.Context, id string) (Workspace, error) { + row := q.db.QueryRowContext(ctx, findWorkspaceByID, id) + var i Workspace + err := row.Scan( + &i.ID, + &i.TenantID, + &i.Name, + &i.CreatedAt, + &i.DeletedAt, + &i.Plan, + &i.StripeCustomerID, + &i.StripeSubscriptionID, + &i.TrialEnds, + &i.BetaFeatures, + &i.Features, + &i.PlanLockedUntil, + &i.PlanDowngradeRequest, + &i.PlanChanged, + &i.Subscriptions, + &i.Enabled, + &i.DeleteProtection, + ) + return i, err +} diff --git a/go/pkg/database/gen/workspace_hard_delete.sql.go b/go/pkg/database/gen/workspace_hard_delete.sql.go new file mode 100644 index 0000000000..654f5aa608 --- /dev/null +++ b/go/pkg/database/gen/workspace_hard_delete.sql.go @@ -0,0 +1,26 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_hard_delete.sql + +package gen + +import ( + "context" + "database/sql" +) + +const hardDeleteWorkspace = `-- name: HardDeleteWorkspace :execresult +DELETE FROM ` + "`" + `workspaces` + "`" + ` +WHERE id = ? +AND delete_protection = false +` + +// HardDeleteWorkspace +// +// DELETE FROM `workspaces` +// WHERE id = ? +// AND delete_protection = false +func (q *Queries) HardDeleteWorkspace(ctx context.Context, id string) (sql.Result, error) { + return q.db.ExecContext(ctx, hardDeleteWorkspace, id) +} diff --git a/go/pkg/database/gen/workspace_insert.sql.go b/go/pkg/database/gen/workspace_insert.sql.go new file mode 100644 index 0000000000..35e70c8422 --- /dev/null +++ b/go/pkg/database/gen/workspace_insert.sql.go @@ -0,0 +1,70 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_insert.sql + +package gen + +import ( + "context" +) + +const insertWorkspace = `-- name: InsertWorkspace :exec +INSERT INTO ` + "`" + `workspaces` + "`" + ` ( + id, + tenant_id, + name, + created_at, + plan, + beta_features, + features, + enabled, + delete_protection +) +VALUES ( + ?, + ?, + ?, + NOW(), + 'free', + '{}', + '{}', + true, + true +) +` + +type InsertWorkspaceParams struct { + ID string `db:"id"` + TenantID string `db:"tenant_id"` + Name string `db:"name"` +} + +// InsertWorkspace +// +// INSERT INTO `workspaces` ( +// id, +// tenant_id, +// name, +// created_at, +// plan, +// beta_features, +// features, +// enabled, +// delete_protection +// ) +// VALUES ( +// ?, +// ?, +// ?, +// NOW(), +// 'free', +// '{}', +// '{}', +// true, +// true +// ) +func (q *Queries) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) error { + _, err := q.db.ExecContext(ctx, insertWorkspace, arg.ID, arg.TenantID, arg.Name) + return err +} diff --git a/go/pkg/database/gen/workspace_soft_delete.sql.go b/go/pkg/database/gen/workspace_soft_delete.sql.go new file mode 100644 index 0000000000..3ad79f6235 --- /dev/null +++ b/go/pkg/database/gen/workspace_soft_delete.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_soft_delete.sql + +package gen + +import ( + "context" + "database/sql" +) + +const softDeleteWorkspace = `-- name: SoftDeleteWorkspace :execresult +UPDATE ` + "`" + `workspaces` + "`" + ` +SET deleted_at = NOW() +WHERE id = ? +AND delete_protection = false +` + +// SoftDeleteWorkspace +// +// UPDATE `workspaces` +// SET deleted_at = NOW() +// WHERE id = ? +// AND delete_protection = false +func (q *Queries) SoftDeleteWorkspace(ctx context.Context, id string) (sql.Result, error) { + return q.db.ExecContext(ctx, softDeleteWorkspace, id) +} diff --git a/go/pkg/database/gen/workspace_update_enabled.sql.go b/go/pkg/database/gen/workspace_update_enabled.sql.go new file mode 100644 index 0000000000..e5ecdd7a3a --- /dev/null +++ b/go/pkg/database/gen/workspace_update_enabled.sql.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_update_enabled.sql + +package gen + +import ( + "context" + "database/sql" +) + +const updateWorkspaceEnabled = `-- name: UpdateWorkspaceEnabled :execresult +UPDATE ` + "`" + `workspaces` + "`" + ` +SET enabled = ? +WHERE id = ? +` + +type UpdateWorkspaceEnabledParams struct { + Enabled bool `db:"enabled"` + ID string `db:"id"` +} + +// UpdateWorkspaceEnabled +// +// UPDATE `workspaces` +// SET enabled = ? +// WHERE id = ? +func (q *Queries) UpdateWorkspaceEnabled(ctx context.Context, arg UpdateWorkspaceEnabledParams) (sql.Result, error) { + return q.db.ExecContext(ctx, updateWorkspaceEnabled, arg.Enabled, arg.ID) +} diff --git a/go/pkg/database/gen/workspace_update_plan.sql.go b/go/pkg/database/gen/workspace_update_plan.sql.go new file mode 100644 index 0000000000..20366def6a --- /dev/null +++ b/go/pkg/database/gen/workspace_update_plan.sql.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: workspace_update_plan.sql + +package gen + +import ( + "context" + "database/sql" +) + +const updateWorkspacePlan = `-- name: UpdateWorkspacePlan :execresult +UPDATE ` + "`" + `workspaces` + "`" + ` +SET plan = ? +WHERE id = ? +` + +type UpdateWorkspacePlanParams struct { + Plan NullWorkspacesPlan `db:"plan"` + ID string `db:"id"` +} + +// UpdateWorkspacePlan +// +// UPDATE `workspaces` +// SET plan = ? +// WHERE id = ? +func (q *Queries) UpdateWorkspacePlan(ctx context.Context, arg UpdateWorkspacePlanParams) (sql.Result, error) { + return q.db.ExecContext(ctx, updateWorkspacePlan, arg.Plan, arg.ID) +} diff --git a/go/pkg/database/interface.go b/go/pkg/database/interface.go new file mode 100644 index 0000000000..24a1c0e387 --- /dev/null +++ b/go/pkg/database/interface.go @@ -0,0 +1,57 @@ +package database + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/entities" +) + +type Database interface { + + // Workspace + InsertWorkspace(ctx context.Context, workspace entities.Workspace) error + FindWorkspaceByID(ctx context.Context, id string) (entities.Workspace, error) + UpdateWorkspacePlan(ctx context.Context, id string, plan entities.WorkspacePlan) error + UpdateWorkspaceEnabled(ctx context.Context, id string, enabled bool) error + // UpdateWorkspace(ctx context.Context, workspace entities.Workspace) error + // FindWorkspace(ctx context.Context, workspaceId string) (entities.Workspace, bool, error) + DeleteWorkspace(ctx context.Context, id string, hardDelete bool) error + + // KeyAuth + // InsertKeyAuth(ctx context.Context, newKeyAuth entities.KeyAuth) error + // DeleteKeyAuth(ctx context.Context, keyAuthId string) error + // FindKeyAuth(ctx context.Context, keyAuthId string) (keyauth entities.KeyAuth, found bool, err error) + + // // Api + // InsertApi(ctx context.Context, api entities.Api) error + // FindApi(ctx context.Context, apiId string) (api entities.Api, found bool, err error) + // DeleteApi(ctx context.Context, apiId string) error + // FindApiByKeyAuthId(ctx context.Context, keyAuthId string) (api entities.Api, found bool, err error) + // ListAllApis(ctx context.Context, limit int, offset int) ([]entities.Api, error) + + // Key + // InsertKey(ctx context.Context, newKey entities.Key) error + FindKeyByID(ctx context.Context, keyId string) (key entities.Key, err error) + FindKeyByHash(ctx context.Context, hash string) (key entities.Key, err error) + FindKeyForVerification(ctx context.Context, hash string) (key entities.KeyForVerification, err error) + // UpdateKey(ctx context.Context, key entities.Key) error + // SoftDeleteKey(ctx context.Context, keyId string) error + // DecrementRemainingKeyUsage(ctx context.Context, keyId string) (key entities.Key, err error) + // CountKeys(ctx context.Context, keyAuthId string) (int64, error) + // ListKeys(ctx context.Context, keyAuthId string, ownerId string, limit int, offset int) ([]entities.Key, error) + + // Ratelimit Namespace + InsertRatelimitNamespace(ctx context.Context, namespace entities.RatelimitNamespace) error + FindRatelimitNamespaceByID(ctx context.Context, id string) (entities.RatelimitNamespace, error) + FindRatelimitNamespaceByName(ctx context.Context, workspaceID string, name string) (entities.RatelimitNamespace, error) + DeleteRatelimitNamespace(ctx context.Context, id string) error + + // Ratelimit Override + InsertRatelimitOverride(ctx context.Context, ratelimitOverride entities.RatelimitOverride) error + FindRatelimitOverrideByIdentifier(ctx context.Context, identifier string) (ratelimitOverride entities.RatelimitOverride, err error) + UpdateRatelimitOverride(ctx context.Context, override entities.RatelimitOverride) error + DeleteRatelimitOverride(ctx context.Context, id string) error + + // Stuff + Close() error +} diff --git a/go/pkg/database/key_find_by_hash.go b/go/pkg/database/key_find_by_hash.go new file mode 100644 index 0000000000..5dd146af8e --- /dev/null +++ b/go/pkg/database/key_find_by_hash.go @@ -0,0 +1,32 @@ +package database + +import ( + "context" + "database/sql" + + "errors" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindKeyByHash(ctx context.Context, hash string) (entities.Key, error) { + + model, err := db.read().FindKeyByHash(ctx, hash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.Key{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", "The key does not exist."), + ) + } + return entities.Key{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + key, err := transform.KeyModelToEntity(model) + if err != nil { + return entities.Key{}, fault.Wrap(err, fault.WithDesc("cannot transform key model to entity", "")) + } + return key, nil +} diff --git a/go/pkg/database/key_find_by_id.go b/go/pkg/database/key_find_by_id.go new file mode 100644 index 0000000000..deaa8ce429 --- /dev/null +++ b/go/pkg/database/key_find_by_id.go @@ -0,0 +1,33 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + + "errors" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindKeyByID(ctx context.Context, keyID string) (entities.Key, error) { + + model, err := db.read().FindKeyByID(ctx, keyID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.Key{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", fmt.Sprintf("The key %s does not exist.", keyID)), + ) + } + return entities.Key{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + key, err := transform.KeyModelToEntity(model) + if err != nil { + return entities.Key{}, fault.Wrap(err, fault.WithDesc("cannot transform key model to entity", "")) + } + return key, nil +} diff --git a/go/pkg/database/middleware.go b/go/pkg/database/middleware.go new file mode 100644 index 0000000000..d23470e9c6 --- /dev/null +++ b/go/pkg/database/middleware.go @@ -0,0 +1,3 @@ +package database + +type Middleware func(Database) Database diff --git a/go/pkg/database/queries/key_find_by_hash.sql b/go/pkg/database/queries/key_find_by_hash.sql new file mode 100644 index 0000000000..d97713f6da --- /dev/null +++ b/go/pkg/database/queries/key_find_by_hash.sql @@ -0,0 +1,3 @@ +-- name: FindKeyByID :one +SELECT * FROM `keys` +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/key_find_by_id.sql b/go/pkg/database/queries/key_find_by_id.sql new file mode 100644 index 0000000000..f4b3c1cbc0 --- /dev/null +++ b/go/pkg/database/queries/key_find_by_id.sql @@ -0,0 +1,3 @@ +-- name: FindRatelimitOverrideByIdentifier :one +SELECT * FROM `ratelimit_overrides` +WHERE identifier = sqlc.arg(identifier); diff --git a/go/pkg/database/queries/ratelimit_namespace_delete.sql b/go/pkg/database/queries/ratelimit_namespace_delete.sql new file mode 100644 index 0000000000..405058d5d6 --- /dev/null +++ b/go/pkg/database/queries/ratelimit_namespace_delete.sql @@ -0,0 +1,4 @@ +-- name: DeleteRatelimitNamespace :execresult +UPDATE `ratelimit_namespaces` +SET deleted_at = NOW() +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/ratelimit_namespace_find_by_id.sql b/go/pkg/database/queries/ratelimit_namespace_find_by_id.sql new file mode 100644 index 0000000000..7792b67b23 --- /dev/null +++ b/go/pkg/database/queries/ratelimit_namespace_find_by_id.sql @@ -0,0 +1,3 @@ +-- name: FindRatelimitNamespaceByID :one +SELECT * FROM `ratelimit_namespaces` +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/ratelimit_namespace_find_by_name.sql b/go/pkg/database/queries/ratelimit_namespace_find_by_name.sql new file mode 100644 index 0000000000..422c9bdae2 --- /dev/null +++ b/go/pkg/database/queries/ratelimit_namespace_find_by_name.sql @@ -0,0 +1,4 @@ +-- name: FindRatelimitNamespaceByName :one +SELECT * FROM `ratelimit_namespaces` +WHERE name = sqlc.arg(name) +AND workspace_id = sqlc.arg(workspace_id); diff --git a/go/pkg/database/queries/ratelimit_override_delete.sql b/go/pkg/database/queries/ratelimit_override_delete.sql new file mode 100644 index 0000000000..58ef9dec7d --- /dev/null +++ b/go/pkg/database/queries/ratelimit_override_delete.sql @@ -0,0 +1,5 @@ +-- name: DeleteRatelimitOverride :execresult +UPDATE `ratelimit_overrides` +SET + deleted_at = NOW() +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/get_key_by_hash.sql b/go/pkg/database/queries/ratelimit_override_find_by_identifier.sql similarity index 64% rename from go/pkg/database/queries/get_key_by_hash.sql rename to go/pkg/database/queries/ratelimit_override_find_by_identifier.sql index 51af943986..8b395d5955 100644 --- a/go/pkg/database/queries/get_key_by_hash.sql +++ b/go/pkg/database/queries/ratelimit_override_find_by_identifier.sql @@ -1,3 +1,3 @@ --- name: GetKeyByHash :one +-- name: FindKeyByHash :one SELECT * FROM `keys` WHERE hash = sqlc.arg(hash); diff --git a/go/pkg/database/queries/insert_override.sql b/go/pkg/database/queries/ratelimit_override_insert.sql similarity index 100% rename from go/pkg/database/queries/insert_override.sql rename to go/pkg/database/queries/ratelimit_override_insert.sql diff --git a/go/pkg/database/queries/ratelimit_override_update.sql b/go/pkg/database/queries/ratelimit_override_update.sql new file mode 100644 index 0000000000..ab27b131cc --- /dev/null +++ b/go/pkg/database/queries/ratelimit_override_update.sql @@ -0,0 +1,8 @@ +-- name: UpdateRatelimitOverride :execresult +UPDATE `ratelimit_overrides` +SET + `limit` = sqlc.arg(windowLimit), + duration = sqlc.arg(duration), + async = sqlc.arg(async), + updated_at = NOW() +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/workspace_find_by_id.sql b/go/pkg/database/queries/workspace_find_by_id.sql new file mode 100644 index 0000000000..82defadbab --- /dev/null +++ b/go/pkg/database/queries/workspace_find_by_id.sql @@ -0,0 +1,3 @@ +-- name: FindWorkspaceByID :one +SELECT * FROM `workspaces` +WHERE id = sqlc.arg(id); diff --git a/go/pkg/database/queries/workspace_hard_delete.sql b/go/pkg/database/queries/workspace_hard_delete.sql new file mode 100644 index 0000000000..642e7ee69d --- /dev/null +++ b/go/pkg/database/queries/workspace_hard_delete.sql @@ -0,0 +1,4 @@ +-- name: HardDeleteWorkspace :execresult +DELETE FROM `workspaces` +WHERE id = sqlc.arg(id) +AND delete_protection = false; diff --git a/go/pkg/database/queries/workspace_insert.sql b/go/pkg/database/queries/workspace_insert.sql new file mode 100644 index 0000000000..6a4c380f42 --- /dev/null +++ b/go/pkg/database/queries/workspace_insert.sql @@ -0,0 +1,23 @@ +-- name: InsertWorkspace :exec +INSERT INTO `workspaces` ( + id, + tenant_id, + name, + created_at, + plan, + beta_features, + features, + enabled, + delete_protection +) +VALUES ( + sqlc.arg(id), + sqlc.arg(tenant_id), + sqlc.arg(name), + NOW(), + 'free', + '{}', + '{}', + true, + true +); diff --git a/go/pkg/database/queries/workspace_soft_delete.sql b/go/pkg/database/queries/workspace_soft_delete.sql new file mode 100644 index 0000000000..c08510a21e --- /dev/null +++ b/go/pkg/database/queries/workspace_soft_delete.sql @@ -0,0 +1,5 @@ +-- name: SoftDeleteWorkspace :execresult +UPDATE `workspaces` +SET deleted_at = NOW() +WHERE id = sqlc.arg(id) +AND delete_protection = false; diff --git a/go/pkg/database/queries/workspace_update_enabled.sql b/go/pkg/database/queries/workspace_update_enabled.sql new file mode 100644 index 0000000000..25fa326613 --- /dev/null +++ b/go/pkg/database/queries/workspace_update_enabled.sql @@ -0,0 +1,5 @@ +-- name: UpdateWorkspaceEnabled :execresult +UPDATE `workspaces` +SET enabled = sqlc.arg(enabled) +WHERE id = sqlc.arg(id) +; diff --git a/go/pkg/database/queries/workspace_update_plan.sql b/go/pkg/database/queries/workspace_update_plan.sql new file mode 100644 index 0000000000..3d2683cd7d --- /dev/null +++ b/go/pkg/database/queries/workspace_update_plan.sql @@ -0,0 +1,5 @@ +-- name: UpdateWorkspacePlan :execresult +UPDATE `workspaces` +SET plan = sqlc.arg(plan) +WHERE id = sqlc.arg(id) +; diff --git a/go/pkg/database/ratelimit_namespace_delete.go b/go/pkg/database/ratelimit_namespace_delete.go new file mode 100644 index 0000000000..07e488cf80 --- /dev/null +++ b/go/pkg/database/ratelimit_namespace_delete.go @@ -0,0 +1,29 @@ +package database + +import ( + "context" + "database/sql" + + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) DeleteRatelimitNamespace(ctx context.Context, id string) error { + result, err := db.write().DeleteRatelimitNamespace(ctx, id) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to delete ratelimit namespace", "")) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to get rows affected", "")) + } + + if rowsAffected == 0 { + return fault.Wrap(sql.ErrNoRows, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("ratelimit namespace not found", ""), + ) + } + + return nil +} diff --git a/go/pkg/database/ratelimit_namespace_find_by_id.go b/go/pkg/database/ratelimit_namespace_find_by_id.go new file mode 100644 index 0000000000..39a2076a33 --- /dev/null +++ b/go/pkg/database/ratelimit_namespace_find_by_id.go @@ -0,0 +1,32 @@ +package database + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindRatelimitNamespaceByID(ctx context.Context, namespaceID string) (entities.RatelimitNamespace, error) { + model, err := db.read().FindRatelimitNamespaceByID(ctx, namespaceID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.RatelimitNamespace{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", fmt.Sprintf("Ratelimit namespace '%s' does not exist.", namespaceID)), + ) + } + return entities.RatelimitNamespace{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + namespace, err := transform.RatelimitNamespaceModelToEntity(model) + if err != nil { + return entities.RatelimitNamespace{}, fault.Wrap(err, + fault.WithDesc("cannot transform namespace model to entity", "")) + } + return namespace, nil +} diff --git a/go/pkg/database/ratelimit_namespace_find_by_name.go b/go/pkg/database/ratelimit_namespace_find_by_name.go new file mode 100644 index 0000000000..5bee21d467 --- /dev/null +++ b/go/pkg/database/ratelimit_namespace_find_by_name.go @@ -0,0 +1,36 @@ +package database + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindRatelimitNamespaceByName(ctx context.Context, workspaceID string, name string) (entities.RatelimitNamespace, error) { + model, err := db.read().FindRatelimitNamespaceByName(ctx, gen.FindRatelimitNamespaceByNameParams{ + WorkspaceID: workspaceID, + Name: name, + }) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.RatelimitNamespace{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", fmt.Sprintf("Ratelimit namespace '%s' does not exist in workspace %s.", name, workspaceID)), + ) + } + return entities.RatelimitNamespace{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + namespace, err := transform.RatelimitNamespaceModelToEntity(model) + if err != nil { + return entities.RatelimitNamespace{}, fault.Wrap(err, + fault.WithDesc("cannot transform namespace model to entity", "")) + } + return namespace, nil +} diff --git a/go/pkg/database/ratelimit_namespace_insert.go b/go/pkg/database/ratelimit_namespace_insert.go new file mode 100644 index 0000000000..f1edabe142 --- /dev/null +++ b/go/pkg/database/ratelimit_namespace_insert.go @@ -0,0 +1,22 @@ +package database + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) InsertRatelimitNamespace(ctx context.Context, namespace entities.RatelimitNamespace) error { + params := transform.RatelimitNamespaceEntityToInsertParams(namespace) + + err := db.write().InsertRatelimitNamespace(ctx, params) + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to insert ratelimit namespace", ""), + ) + } + + return nil +} diff --git a/go/pkg/database/ratelimit_override_delete.go b/go/pkg/database/ratelimit_override_delete.go new file mode 100644 index 0000000000..351d537f49 --- /dev/null +++ b/go/pkg/database/ratelimit_override_delete.go @@ -0,0 +1,29 @@ +package database + +import ( + "context" + "database/sql" + + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) DeleteRatelimitOverride(ctx context.Context, id string) error { + result, err := db.write().DeleteRatelimitOverride(ctx, id) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to delete ratelimit override", "")) + } + + rows, err := result.RowsAffected() + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to get rows affected", "")) + } + + if rows == 0 { + return fault.Wrap(sql.ErrNoRows, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("ratelimit override not found", ""), + ) + } + + return nil +} diff --git a/go/pkg/database/ratelimit_override_find_by_identifier.go b/go/pkg/database/ratelimit_override_find_by_identifier.go new file mode 100644 index 0000000000..2cd9ab7dbb --- /dev/null +++ b/go/pkg/database/ratelimit_override_find_by_identifier.go @@ -0,0 +1,33 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + + "errors" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindRatelimitOverrideByIdentifier(ctx context.Context, identifier string) (entities.RatelimitOverride, error) { + + model, err := db.read().FindRatelimitOverrideByIdentifier(ctx, identifier) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.RatelimitOverride{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", fmt.Sprintf("An override for %s does not exist.", identifier)), + ) + } + return entities.RatelimitOverride{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + e, err := transform.RatelimitOverrideModelToEntity(model) + if err != nil { + return entities.RatelimitOverride{}, fault.Wrap(err, fault.WithDesc("cannot transform model to entity", "")) + } + return e, nil +} diff --git a/go/pkg/database/ratelimit_override_insert.go b/go/pkg/database/ratelimit_override_insert.go new file mode 100644 index 0000000000..e46bd2a7c6 --- /dev/null +++ b/go/pkg/database/ratelimit_override_insert.go @@ -0,0 +1,28 @@ +package database + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) InsertRatelimitOverride(ctx context.Context, override entities.RatelimitOverride) error { + + err := db.write().InsertOverride(ctx, gen.InsertOverrideParams{ + ID: override.ID, + WorkspaceID: override.WorkspaceID, + NamespaceID: override.NamespaceID, + Identifier: override.Identifier, + Limit: override.Limit, + Duration: int32(override.Duration.Milliseconds()), // nolint:gosec + }) + if err != nil { + + return fault.Wrap(err, + fault.WithDesc("failed inserting", ""), + ) + } + return nil +} diff --git a/go/pkg/database/ratelimit_override_update.go b/go/pkg/database/ratelimit_override_update.go new file mode 100644 index 0000000000..7d1cc0fd71 --- /dev/null +++ b/go/pkg/database/ratelimit_override_update.go @@ -0,0 +1,42 @@ +package database + +import ( + "context" + "database/sql" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) UpdateRatelimitOverride(ctx context.Context, e entities.RatelimitOverride) error { + params := gen.UpdateRatelimitOverrideParams{ + ID: e.ID, + Windowlimit: e.Limit, + + Duration: int32(e.Duration.Milliseconds()), // nolint:gosec + Async: sql.NullBool{ + Bool: e.Async, + Valid: true, + }, + } + + result, err := db.write().UpdateRatelimitOverride(ctx, params) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to update ratelimit override", "")) + } + + rows, err := result.RowsAffected() + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to get rows affected", "")) + } + + if rows == 0 { + return fault.Wrap(sql.ErrNoRows, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("ratelimit override not found", ""), + ) + } + + return nil +} diff --git a/go/pkg/database/schema_embed.go b/go/pkg/database/schema_embed.go new file mode 100644 index 0000000000..571da808de --- /dev/null +++ b/go/pkg/database/schema_embed.go @@ -0,0 +1,10 @@ +package database + +import ( + _ "embed" +) + +// Schema is the sql schema embedded into the binary +// +//go:embed schema.sql +var Schema []byte diff --git a/go/pkg/database/sqlc.json b/go/pkg/database/sqlc.json index 403ac21c01..41c1801008 100644 --- a/go/pkg/database/sqlc.json +++ b/go/pkg/database/sqlc.json @@ -10,6 +10,7 @@ "package": "gen", "out": "gen", "sql_package": "database/sql", + "query_parameter_limit": 1, "emit_db_tags": true, "emit_interface": true, "emit_sql_as_comment": true diff --git a/go/pkg/database/transform/key.go b/go/pkg/database/transform/key.go new file mode 100644 index 0000000000..db633dee5d --- /dev/null +++ b/go/pkg/database/transform/key.go @@ -0,0 +1,65 @@ +package transform + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" +) + +func KeyModelToEntity(m gen.Key) (entities.Key, error) { + + key := entities.Key{ + ID: m.ID, + KeySpaceID: m.KeyAuthID, + WorkspaceID: m.WorkspaceID, + Hash: m.Hash, + Start: m.Start, + CreatedAt: m.CreatedAt, + ForWorkspaceID: "", + Name: "", + Enabled: m.Enabled, + IdentityID: "", + Meta: map[string]any{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + Environment: "", + Expires: time.Time{}, + } + + if m.Name.Valid { + key.Name = m.Name.String + } + + if m.Meta.Valid { + err := json.Unmarshal([]byte(m.Meta.String), &key.Meta) + if err != nil { + return entities.Key{}, fmt.Errorf("uanble to unmarshal meta: %w", err) + } + } + if m.Expires.Valid { + key.Expires = m.Expires.Time + } + + if m.ForWorkspaceID.Valid { + key.ForWorkspaceID = m.ForWorkspaceID.String + } + if m.IdentityID.Valid { + key.IdentityID = m.IdentityID.String + } + + if m.UpdatedAtM.Valid { + key.UpdatedAt = time.UnixMilli(m.UpdatedAtM.Int64) + } + + if m.DeletedAtM.Valid { + key.DeletedAt = time.UnixMilli(m.DeletedAtM.Int64) + } + if m.Environment.Valid { + key.Environment = m.Environment.String + } + + return key, nil +} diff --git a/go/pkg/database/transform/ratelimit_namespace.go b/go/pkg/database/transform/ratelimit_namespace.go new file mode 100644 index 0000000000..a76d93c366 --- /dev/null +++ b/go/pkg/database/transform/ratelimit_namespace.go @@ -0,0 +1,37 @@ +package transform + +import ( + "time" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" +) + +func RatelimitNamespaceModelToEntity(m gen.RatelimitNamespace) (entities.RatelimitNamespace, error) { + namespace := entities.RatelimitNamespace{ + ID: m.ID, + WorkspaceID: m.WorkspaceID, + Name: m.Name, + CreatedAt: m.CreatedAt, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + if m.UpdatedAt.Valid { + namespace.UpdatedAt = m.UpdatedAt.Time + } + + if m.DeletedAt.Valid { + namespace.DeletedAt = m.DeletedAt.Time + } + + return namespace, nil +} + +func RatelimitNamespaceEntityToInsertParams(e entities.RatelimitNamespace) gen.InsertRatelimitNamespaceParams { + return gen.InsertRatelimitNamespaceParams{ + ID: e.ID, + WorkspaceID: e.WorkspaceID, + Name: e.Name, + } +} diff --git a/go/pkg/database/transform/ratelimit_override.go b/go/pkg/database/transform/ratelimit_override.go new file mode 100644 index 0000000000..4ed42751b3 --- /dev/null +++ b/go/pkg/database/transform/ratelimit_override.go @@ -0,0 +1,64 @@ +package transform + +import ( + "database/sql" + "time" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" +) + +func RatelimitOverrideModelToEntity(m gen.RatelimitOverride) (entities.RatelimitOverride, error) { + e := entities.RatelimitOverride{ + ID: m.ID, + WorkspaceID: m.WorkspaceID, + NamespaceID: m.NamespaceID, + Identifier: m.Identifier, + Limit: m.Limit, + Duration: time.Duration(m.Duration) * time.Millisecond, + Async: m.Async.Bool, + CreatedAt: m.CreatedAt, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + } + + if m.UpdatedAt.Valid { + e.UpdatedAt = m.UpdatedAt.Time + } + + if m.DeletedAt.Valid { + e.DeletedAt = m.DeletedAt.Time + } + + return e, nil +} + +func RatelimitOverrideEntityToModel(e entities.RatelimitOverride) gen.RatelimitOverride { + m := gen.RatelimitOverride{ + ID: e.ID, + WorkspaceID: e.WorkspaceID, + NamespaceID: e.NamespaceID, + Identifier: e.Identifier, + Limit: e.Limit, + Duration: int32(e.Duration.Milliseconds()), // nolint:gosec + Async: sql.NullBool{ + Bool: e.Async, + Valid: true, + }, + CreatedAt: e.CreatedAt, + UpdatedAt: sql.NullTime{ + Time: e.UpdatedAt, + Valid: !e.UpdatedAt.IsZero(), + }, + DeletedAt: sql.NullTime{ + Time: e.DeletedAt, + Valid: !e.DeletedAt.IsZero(), + }, + Sharding: gen.NullRatelimitOverridesSharding{ + RatelimitOverridesSharding: gen.RatelimitOverridesSharding("edge"), + Valid: false, + }, + } + + return m +} diff --git a/go/pkg/database/transform/workspace.go b/go/pkg/database/transform/workspace.go new file mode 100644 index 0000000000..7f46747da6 --- /dev/null +++ b/go/pkg/database/transform/workspace.go @@ -0,0 +1,73 @@ +package transform + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" +) + +func WorkspaceModelToEntity(m gen.Workspace) (entities.Workspace, error) { + workspace := entities.Workspace{ + ID: m.ID, + TenantID: m.TenantID, + Name: m.Name, + Enabled: m.Enabled, + DeleteProtection: true, + CreatedAt: time.Time{}, + DeletedAt: time.Time{}, + Plan: entities.WorkspacePlan(m.Plan.WorkspacesPlan), + StripeCustomerID: "", + StripeSubscriptionID: "", + TrialEnds: time.Time{}, + PlanLockedUntil: time.Time{}, + BetaFeatures: map[string]any{}, + Features: map[string]any{}, + } + + if m.CreatedAt.Valid { + workspace.CreatedAt = m.CreatedAt.Time + } + + if m.DeletedAt.Valid { + workspace.DeletedAt = m.DeletedAt.Time + } + + if m.Plan.Valid { + workspace.Plan = entities.WorkspacePlan(m.Plan.WorkspacesPlan) + } else { + workspace.Plan = entities.WorkspacePlanFree + } + + if m.DeleteProtection.Valid { + workspace.DeleteProtection = m.DeleteProtection.Bool + } + + if m.StripeCustomerID.Valid { + workspace.StripeCustomerID = m.StripeCustomerID.String + } + + if m.StripeSubscriptionID.Valid { + workspace.StripeSubscriptionID = m.StripeSubscriptionID.String + } + + if m.TrialEnds.Valid { + workspace.TrialEnds = m.TrialEnds.Time + } + + if m.PlanLockedUntil.Valid { + workspace.PlanLockedUntil = m.PlanLockedUntil.Time + } + + if err := json.Unmarshal(m.BetaFeatures, &workspace.BetaFeatures); err != nil { + return entities.Workspace{}, fmt.Errorf("unable to unmarshal beta features: %w", err) + } + + if err := json.Unmarshal(m.Features, &workspace.Features); err != nil { + return entities.Workspace{}, fmt.Errorf("unable to unmarshal features: %w", err) + } + + return workspace, nil +} diff --git a/go/pkg/database/workspace_delete.go b/go/pkg/database/workspace_delete.go new file mode 100644 index 0000000000..a807100e1e --- /dev/null +++ b/go/pkg/database/workspace_delete.go @@ -0,0 +1,57 @@ +package database + +import ( + "context" + "log/slog" + + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) DeleteWorkspace(ctx context.Context, id string, hardDelete bool) error { + tx, err := db.writeReplica.db.BeginTx(ctx, nil) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to start transaction", "")) + } + defer func() { + rollbackErr := tx.Rollback() + if rollbackErr != nil { + db.logger.Error(ctx, "failed to rollback transaction", slog.String("error", rollbackErr.Error())) + } + }() + qtx := db.write().WithTx(tx) + + // Check protection within the transaction + workspace, err := qtx.FindWorkspaceByID(ctx, id) + if err != nil { + return fault.Wrap(err, + + fault.WithDesc("failed to load workspace", ""), + ) + } + + if workspace.DeleteProtection.Valid && workspace.DeleteProtection.Bool { + return fault.New("unable to delete workspace", + fault.WithTag(fault.PROTECTED_RESOURCE), + fault.WithDesc("workspace is protected", "This workspace has delete protection enabled and cannot be deleted."), + ) + } + if hardDelete { + _, err = qtx.HardDeleteWorkspace(ctx, id) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to hard delete workspace", "")) + } + } else { + _, err = qtx.SoftDeleteWorkspace(ctx, id) + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to soft delete workspace", "")) + } + + } + + err = tx.Commit() + if err != nil { + return fault.Wrap(err, fault.WithDesc("failed to commit transaction", "")) + } + + return nil +} diff --git a/go/pkg/database/workspace_find_by_id.go b/go/pkg/database/workspace_find_by_id.go new file mode 100644 index 0000000000..8f29f7dde6 --- /dev/null +++ b/go/pkg/database/workspace_find_by_id.go @@ -0,0 +1,32 @@ +package database + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "github.com/unkeyed/unkey/go/pkg/database/transform" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) FindWorkspaceByID(ctx context.Context, id string) (entities.Workspace, error) { + model, err := db.read().FindWorkspaceByID(ctx, id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return entities.Workspace{}, fault.Wrap(err, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("not found", fmt.Sprintf("Workspace with ID %s does not exist.", id)), + ) + } + return entities.Workspace{}, fault.Wrap(err, fault.WithTag(fault.DATABASE_ERROR)) + } + + workspace, err := transform.WorkspaceModelToEntity(model) + if err != nil { + return entities.Workspace{}, fault.Wrap(err, + fault.WithDesc("cannot transform workspace model to entity", "")) + } + return workspace, nil +} diff --git a/go/pkg/database/workspace_insert.go b/go/pkg/database/workspace_insert.go new file mode 100644 index 0000000000..b0b7df3efe --- /dev/null +++ b/go/pkg/database/workspace_insert.go @@ -0,0 +1,27 @@ +package database + +import ( + "context" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) InsertWorkspace(ctx context.Context, workspace entities.Workspace) error { + + params := gen.InsertWorkspaceParams{ + ID: workspace.ID, + TenantID: workspace.TenantID, + Name: workspace.Name, + } + + err := db.write().InsertWorkspace(ctx, params) + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to insert workspace", ""), + ) + } + + return nil +} diff --git a/go/pkg/database/workspace_update.go b/go/pkg/database/workspace_update.go new file mode 100644 index 0000000000..1aa818658f --- /dev/null +++ b/go/pkg/database/workspace_update.go @@ -0,0 +1,68 @@ +package database + +import ( + "context" + "database/sql" + + "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/fault" +) + +func (db *database) UpdateWorkspacePlan(ctx context.Context, id string, plan entities.WorkspacePlan) error { + result, err := db.write().UpdateWorkspacePlan(ctx, gen.UpdateWorkspacePlanParams{ + ID: id, + Plan: gen.NullWorkspacesPlan{ + WorkspacesPlan: gen.WorkspacesPlan(plan), + Valid: true, + }}) + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to update workspace plan", ""), + ) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to get rows affected", ""), + ) + } + + if rowsAffected == 0 { + return fault.Wrap(sql.ErrNoRows, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("workspace not found", "The workspace you're trying to update doesn't exist or has been deleted."), + ) + } + + return nil +} + +func (db *database) UpdateWorkspaceEnabled(ctx context.Context, id string, enabled bool) error { + result, err := db.write().UpdateWorkspaceEnabled(ctx, gen.UpdateWorkspaceEnabledParams{ + ID: id, + Enabled: enabled, + }) + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to update workspace enabled status", ""), + ) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fault.Wrap(err, + fault.WithDesc("failed to get rows affected", ""), + ) + } + + if rowsAffected == 0 { + return fault.Wrap(sql.ErrNoRows, + fault.WithTag(fault.NOT_FOUND), + fault.WithDesc("workspace not found", "The workspace you're trying to update doesn't exist or has been deleted."), + ) + } + + return nil +} diff --git a/go/pkg/entities/key.go b/go/pkg/entities/key.go new file mode 100644 index 0000000000..11b1b3f15a --- /dev/null +++ b/go/pkg/entities/key.go @@ -0,0 +1,54 @@ +package entities + +import "time" + +// Key represents an API key in the system +type Key struct { + // ID is the unique identifier for the key + ID string + + // KeySpaceID represents the key authorization space this key belongs to + KeySpaceID string + + // WorkspaceID is the ID of the workspace that owns this key + WorkspaceID string + + // Hash is the secure hash of the key used for verification + Hash string + + // Start is the prefix of the key shown to users for identification + Start string + + // ForWorkspaceID is used only for internal keys to indicate which workspace the key is for + // This is primarily used for managing the Unkey app itself and is not used for user keys + ForWorkspaceID string + + // Name is an optional human-readable identifier for the key + Name string + + // IdentityID links this key to a specific identity in the system + IdentityID string + + // Meta contains arbitrary metadata associated with the key as key-value pairs + Meta map[string]any + + // CreatedAt is the timestamp when the key was created + CreatedAt time.Time + + // UpdatedAt is the timestamp when the key was last modified + UpdatedAt time.Time + + // DeletedAt indicates when the key was revoked + // A zero time value means the key is not deleted. + DeletedAt time.Time + + // Enabled indicates whether the key is currently active (true) or disabled (false) + // Keys are enabled by default. + Enabled bool + + // Environment is an optional flag used to segment keys (e.g., "test" vs "production") + // This is a user-defined value with no system-level restrictions + Environment string + + Expires time.Time +} diff --git a/go/pkg/entities/ratelimit_namespace.go b/go/pkg/entities/ratelimit_namespace.go new file mode 100644 index 0000000000..0a8e076093 --- /dev/null +++ b/go/pkg/entities/ratelimit_namespace.go @@ -0,0 +1,12 @@ +package entities + +import "time" + +type RatelimitNamespace struct { + ID string + WorkspaceID string + Name string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} diff --git a/go/pkg/entities/ratelimit_override.go b/go/pkg/entities/ratelimit_override.go new file mode 100644 index 0000000000..e8e316105d --- /dev/null +++ b/go/pkg/entities/ratelimit_override.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" +) + +type RatelimitOverride struct { + ID string + WorkspaceID string + NamespaceID string + Identifier string + Limit int32 + Duration time.Duration + Async bool + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt time.Time +} diff --git a/go/pkg/entities/workspace.go b/go/pkg/entities/workspace.go new file mode 100644 index 0000000000..a345e25fd0 --- /dev/null +++ b/go/pkg/entities/workspace.go @@ -0,0 +1,28 @@ +package entities + +import "time" + +type WorkspacePlan string + +const ( + WorkspacePlanFree WorkspacePlan = "free" + WorkspacePlanPro WorkspacePlan = "pro" + WorkspacePlanEnterprise WorkspacePlan = "enterprise" +) + +type Workspace struct { + ID string + TenantID string + Name string + CreatedAt time.Time + DeletedAt time.Time + Plan WorkspacePlan + Enabled bool + DeleteProtection bool + BetaFeatures map[string]interface{} + Features map[string]interface{} + StripeCustomerID string + StripeSubscriptionID string + TrialEnds time.Time + PlanLockedUntil time.Time +} diff --git a/go/pkg/fault/flatten.go b/go/pkg/fault/flatten.go new file mode 100644 index 0000000000..e5a81acede --- /dev/null +++ b/go/pkg/fault/flatten.go @@ -0,0 +1,49 @@ +package fault + +import ( + "slices" +) + +type Step struct { + Message string + Location string +} + +func Flatten(err error) []Step { + if err == nil { + return []Step{} + } + + current, ok := err.(*wrapped) + if !ok { + return []Step{} + } + + steps := []Step{} + + for current != nil { + steps = append(steps, Step{ + Message: current.internal, + Location: current.location, + }) + + // Check if there's a next error in the chain + if current.err == nil { + break + } + + // Try to get the next wrapped error + next, ok := current.err.(*wrapped) + if !ok { + // if it's not a wrapepd error, then we don't have any more public messages + // and can stop looking. + break + } + current = next + } + + slices.Reverse(steps) + + return steps + +} diff --git a/go/pkg/fault/tag.go b/go/pkg/fault/tag.go index 1ee3ad0939..3c99c8a4a1 100644 --- a/go/pkg/fault/tag.go +++ b/go/pkg/fault/tag.go @@ -12,6 +12,25 @@ const ( // This ensures all errors have at least a basic classification, making error // handling more predictable. UNTAGGED Tag = "UNTAGGED" + + // An object was not found in the system. + NOT_FOUND Tag = "NOT_FOUND" + + UNAUTHORIZED Tag = "UNAUTHORIZED" + FORBIDDEN Tag = "FORBIDDEN" + + DATABASE_ERROR Tag = "DATABASE_ERROR" + + INTERNAL_SERVER_ERROR Tag = "INTERNAL_SERVER_ERROR" + + PROTECTED_RESOURCE Tag = "PROTECTED_RESOURCE" + + // An assertion failed during runtime. + // This tag is used to indicate that a condition that should have been true + // was false, indicating a programming error. + // + // For example we assert that a field on a struct is not empty, but it is. + ASSERTION_FAILED Tag = "ASSERTION_FAILED" ) // GetTag examines an error and its chain of wrapped errors to find the first diff --git a/go/pkg/logging/noop.go b/go/pkg/logging/noop.go new file mode 100644 index 0000000000..c09d3087ec --- /dev/null +++ b/go/pkg/logging/noop.go @@ -0,0 +1,31 @@ +package logging + +import ( + "context" + "log/slog" +) + +type noop struct { +} + +func NewNoop() Logger { + return &noop{} +} + +func (l *noop) With(attrs ...slog.Attr) Logger { + + return l + +} + +func (l *noop) Debug(ctx context.Context, message string, attrs ...slog.Attr) { + +} +func (l *noop) Info(ctx context.Context, message string, attrs ...slog.Attr) { + +} +func (l *noop) Warn(ctx context.Context, message string, attrs ...slog.Attr) { + +} +func (l *noop) Error(ctx context.Context, message string, attrs ...slog.Attr) { +} diff --git a/go/pkg/repeat/every.go b/go/pkg/repeat/every.go new file mode 100644 index 0000000000..cea84cf277 --- /dev/null +++ b/go/pkg/repeat/every.go @@ -0,0 +1,16 @@ +package repeat + +import "time" + +// Every runs the given function in a go routine every d duration until the returned function is called. +func Every(d time.Duration, fn func()) func() { + t := time.NewTicker(d) + go func() { + for range t.C { + fn() + } + }() + return func() { + t.Stop() + } +} diff --git a/go/pkg/testutil/containers.go b/go/pkg/testutil/containers.go new file mode 100644 index 0000000000..6c76aeccec --- /dev/null +++ b/go/pkg/testutil/containers.go @@ -0,0 +1,84 @@ +package testutil + +import ( + "database/sql" + "fmt" + "strings" + "testing" + + _ "github.com/go-sql-driver/mysql" + "github.com/unkeyed/unkey/go/pkg/database" + + "github.com/ory/dockertest/v3" + "github.com/stretchr/testify/require" +) + +type Containers struct { + t *testing.T + pool *dockertest.Pool +} + +func NewContainers(t *testing.T) *Containers { + pool, err := dockertest.NewPool("") + require.NoError(t, err) + + err = pool.Client.Ping() + require.NoError(t, err) + + c := &Containers{ + t: t, + pool: pool, + } + + return c +} + +func (c *Containers) RunMySQL() string { + c.t.Helper() + + resource, err := c.pool.Run("mysql", "latest", []string{ + "MYSQL_ROOT_PASSWORD=root", + "MYSQL_DATABASE=unkey", + "MYSQL_USER=unkey", + "MYSQL_PASSWORD=password", + }) + require.NoError(c.t, err) + + c.t.Cleanup(func() { + require.NoError(c.t, c.pool.Purge(resource)) + }) + + addr := fmt.Sprintf("unkey:password@(localhost:%s)/unkey", resource.GetPort("3306/tcp")) + + var db *sql.DB + require.NoError(c.t, c.pool.Retry(func() error { + var err error + db, err = sql.Open("mysql", addr) + if err != nil { + return fmt.Errorf("unable to open mysql conenction: %w", err) + } + err = db.Ping() + if err != nil { + return fmt.Errorf("unable to ping mysql: %w", err) + } + + return nil + })) + + // Creating the database tables + queries := strings.Split(string(database.Schema), ";") + for _, query := range queries { + query = strings.TrimSpace(query) + if query == "" { + continue + } + // Add the semicolon back + query += ";" + + _, err = db.Exec(query) + require.NoError(c.t, err) + + } + + return addr +} diff --git a/go/pkg/testutil/http.go b/go/pkg/testutil/http.go new file mode 100644 index 0000000000..a57b13611f --- /dev/null +++ b/go/pkg/testutil/http.go @@ -0,0 +1,94 @@ +package testutil + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/pkg/logging" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +type Harness struct { + t *testing.T + + logger logging.Logger + + srv *zen.Server +} + +func NewHarness(t *testing.T) *Harness { + + logger := logging.NewNoop() + + srv, err := zen.New(zen.Config{ + NodeId: "test", + Logger: logger, + Clickhouse: nil, + }) + require.NoError(t, err) + + h := Harness{ + t: t, + logger: logger, + srv: srv, + } + + return &h +} + +func (h *Harness) Register(route zen.Route) { + + h.srv.RegisterRoute(route) + +} + +// Post is a helper function to make a POST request to the API. +// It will hanndle serializing the request and response objects to and from JSON. +func UnmarshalBody[Body any](t *testing.T, r *httptest.ResponseRecorder, body *Body) { + + err := json.Unmarshal(r.Body.Bytes(), &body) + require.NoError(t, err) + +} + +type TestResponse[TBody any] struct { + Status int + Headers http.Header + Body TBody +} + +func CallRoute[Req any, Res any](h *Harness, route zen.Route, headers http.Header, req Req) TestResponse[Res] { + h.t.Helper() + + rr := httptest.NewRecorder() + + body := new(bytes.Buffer) + err := json.NewEncoder(body).Encode(req) + require.NoError(h.t, err) + + httpReq := httptest.NewRequest(route.Method(), route.Path(), body) + httpReq.Header = headers + if httpReq.Header == nil { + httpReq.Header = http.Header{} + } + if route.Method() == http.MethodPost { + httpReq.Header.Set("Content-Type", "application/json") + } + + h.srv.Mux().ServeHTTP(rr, httpReq) + require.NoError(h.t, err) + + var res Res + err = json.NewDecoder(rr.Body).Decode(&res) + require.NoError(h.t, err) + + return TestResponse[Res]{ + Status: rr.Code, + Headers: rr.Header(), + Body: res, + } +} diff --git a/go/pkg/tracing/axiom.go b/go/pkg/tracing/axiom.go new file mode 100644 index 0000000000..c4e7ca4ed7 --- /dev/null +++ b/go/pkg/tracing/axiom.go @@ -0,0 +1,30 @@ +package tracing + +import ( + "context" + "fmt" + + axiom "github.com/axiomhq/axiom-go/axiom/otel" +) + +type Config struct { + Dataset string + Application string + Version string + AxiomToken string +} + +// Closer is a function that closes the global tracer. +type Closer func() error + +func Init(ctx context.Context, config Config) (Closer, error) { + tp, err := axiom.TracerProvider(ctx, config.Dataset, config.Application, config.Version, axiom.SetNoEnv(), axiom.SetToken(config.AxiomToken)) + if err != nil { + return nil, fmt.Errorf("unable to init tracing: %w", err) + } + globalTracer = tp + + return func() error { + return tp.Shutdown(context.Background()) + }, nil +} diff --git a/go/pkg/tracing/schema.go b/go/pkg/tracing/schema.go new file mode 100644 index 0000000000..5d0674f53b --- /dev/null +++ b/go/pkg/tracing/schema.go @@ -0,0 +1,7 @@ +package tracing + +import "fmt" + +func NewSpanName(pkg string, method string) string { + return fmt.Sprintf("%s.%s", pkg, method) +} diff --git a/go/pkg/tracing/trace.go b/go/pkg/tracing/trace.go new file mode 100644 index 0000000000..4148105ff0 --- /dev/null +++ b/go/pkg/tracing/trace.go @@ -0,0 +1,24 @@ +package tracing + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +var globalTracer trace.TracerProvider + +func init() { + globalTracer = noop.NewTracerProvider() +} + +func Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + // nolint:spancheck // the caller will end the span + return globalTracer.Tracer("main").Start(ctx, name, opts...) + +} + +func GetGlobalTraceProvider() trace.TracerProvider { + return globalTracer +} diff --git a/go/pkg/tracing/util.go b/go/pkg/tracing/util.go new file mode 100644 index 0000000000..484e14c67e --- /dev/null +++ b/go/pkg/tracing/util.go @@ -0,0 +1,14 @@ +package tracing + +import ( + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// RecordError sets the status of the span to error if the error is not nil. +func RecordError(span trace.Span, err error) { + if err == nil { + return + } + span.SetStatus(codes.Error, err.Error()) +} diff --git a/go/pkg/uid/uid.go b/go/pkg/uid/uid.go index 1b4a4f5771..4991dfab20 100644 --- a/go/pkg/uid/uid.go +++ b/go/pkg/uid/uid.go @@ -9,24 +9,30 @@ import ( type Prefix string const ( - RequestPrefix Prefix = "req" - NodePrefix Prefix = "node" + RequestPrefix Prefix = "req" + NodePrefix Prefix = "node" + RatelimitOverridePrefix Prefix = "rlor" + TestPrefix Prefix = "test" ) // New Returns a new random base58 encoded uuid. -func New(prefix string) string { +func New(prefix Prefix) string { id := ksuid.New().String() if prefix != "" { - return strings.Join([]string{prefix, id}, "_") + return strings.Join([]string{string(prefix), id}, "_") } else { return id } } func Node() string { - return New(string(NodePrefix)) + return New(NodePrefix) } func Request() string { - return New(string(RequestPrefix)) + return New(RequestPrefix) +} + +func Test() string { + return New(TestPrefix) } diff --git a/go/pkg/zen/errors.go b/go/pkg/zen/errors.go deleted file mode 100644 index a6fcec4812..0000000000 --- a/go/pkg/zen/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package zen - -import "github.com/unkeyed/unkey/go/pkg/fault" - -// Error tags for common error scenarios. -var ( - NotFoundError = fault.Tag("NOT_FOUND_ERROR") - - // DatabaseError represents errors that occur during database operations. - // This error tag is used when database operations fail, such as connection - // issues, query failures, or data integrity problems. - DatabaseError = fault.Tag("DATABASE_ERROR") -) diff --git a/go/pkg/zen/middleware_errors.go b/go/pkg/zen/middleware_errors.go index 55c848aa3e..5ba212bba1 100644 --- a/go/pkg/zen/middleware_errors.go +++ b/go/pkg/zen/middleware_errors.go @@ -1,6 +1,8 @@ package zen import ( + "net/http" + "github.com/unkeyed/unkey/go/api" "github.com/unkeyed/unkey/go/pkg/fault" ) @@ -14,8 +16,8 @@ func WithErrorHandling() Middleware { } switch fault.GetTag(err) { - case NotFoundError: - return s.JSON(404, api.NotFoundError{ + case fault.NOT_FOUND: + return s.JSON(http.StatusNotFound, api.NotFoundError{ Title: "Not Found", Type: "https://unkey.com/docs/errors/not_found", Detail: fault.UserFacingMessage(err), @@ -24,14 +26,27 @@ func WithErrorHandling() Middleware { Instance: nil, }) - case DatabaseError: - // ... + case fault.PROTECTED_RESOURCE: + return s.JSON(http.StatusPreconditionFailed, api.PreconditionFailedError{ + Title: "Resource is protected", + Type: "https://unkey.com/docs/errors/deletion_prevented", + Detail: fault.UserFacingMessage(err), + RequestId: s.requestID, + Status: s.responseStatus, + Instance: nil, + }) + + case fault.DATABASE_ERROR: + break // fall through to default 500 case fault.UNTAGGED: break // fall through to default 500 + + case fault.INTERNAL_SERVER_ERROR: + break } - return s.JSON(500, api.InternalServerError{ + return s.JSON(http.StatusInternalServerError, api.InternalServerError{ Title: "Internal Server Error", Type: "https://unkey.com/docs/errors/internal_server_error", Detail: fault.UserFacingMessage(err), diff --git a/go/pkg/zen/routes/v2_ratelimit_set_override/handler.go b/go/pkg/zen/routes/v2_ratelimit_set_override/handler.go index af7a5b54c6..2218bc510e 100644 --- a/go/pkg/zen/routes/v2_ratelimit_set_override/handler.go +++ b/go/pkg/zen/routes/v2_ratelimit_set_override/handler.go @@ -1,11 +1,15 @@ -package v2RatelimitLimit +package handler import ( "net/http" + "time" "github.com/unkeyed/unkey/go/api" - "github.com/unkeyed/unkey/go/pkg/database/gen" + "github.com/unkeyed/unkey/go/hash" + "github.com/unkeyed/unkey/go/pkg/entities" "github.com/unkeyed/unkey/go/pkg/fault" + "github.com/unkeyed/unkey/go/pkg/hash" + "github.com/unkeyed/unkey/go/pkg/uid" "github.com/unkeyed/unkey/go/pkg/zen" ) @@ -14,22 +18,48 @@ type Response = api.V2RatelimitSetOverrideResponseBody func New(svc *zen.Services) zen.Route { return zen.NewRoute("POST", "/v2/ratelimit.setOverride", func(s *zen.Session) error { - err := svc.Database.InsertOverride(s.Context(), gen.InsertOverrideParams{ - ID: "", - WorkspaceID: "", + + rootKey, err := zen.Bearer(s) + if err != nil { + return err + } + + auth, err := svc.Keys.Verify(s.Context(), hash.Sha256(rootKey)) + if err != nil { + return err + } + + // nolint:exhaustruct + req := Request{} + err = s.BindBody(&req) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("invalid request body", "The request body is invalid."), + ) + } + + overrideID := uid.New(uid.RatelimitOverridePrefix) + err = svc.Database.InsertRatelimitOverride(s.Context(), entities.RatelimitOverride{ + ID: overrideID, + WorkspaceID: auth.AuthorizedWorkspaceID, NamespaceID: "", Identifier: "", Limit: 0, Duration: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + Async: false, }) if err != nil { return fault.Wrap(err, - fault.WithTag(zen.DatabaseError), + fault.WithTag(fault.DATABASE_ERROR), fault.WithDesc("database failed", "The database is unavailable."), ) } return s.JSON(http.StatusOK, Response{ - OverrideId: "", + OverrideId: overrideID, }) }) } diff --git a/go/pkg/zen/routes/v2_ratelimit_set_override/happy_test.go b/go/pkg/zen/routes/v2_ratelimit_set_override/happy_test.go new file mode 100644 index 0000000000..70400112f2 --- /dev/null +++ b/go/pkg/zen/routes/v2_ratelimit_set_override/happy_test.go @@ -0,0 +1,61 @@ +package handler_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/apps/agent/pkg/util" + "github.com/unkeyed/unkey/go/pkg/database" + "github.com/unkeyed/unkey/go/pkg/entities" + "github.com/unkeyed/unkey/go/pkg/logging" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" + handler "github.com/unkeyed/unkey/go/pkg/zen/routes/v2_ratelimit_set_override" +) + +func TestCreateNewOverride(t *testing.T) { + ctx := context.Background() + h := testutil.NewHarness(t) + + c := testutil.NewContainers(t) + + mysqlAddr := c.RunMySQL() + + db, err := database.New(database.Config{ + PrimaryDSN: mysqlAddr, + ReadOnlyDSN: "", + Logger: logging.NewNoop(), + }) + require.NoError(t, err) + + db.InsertRatelimitOverride(ctx, entities.RatelimitOverride{ + ID: uid.Test(), + WorkspaceID: uid.Test(), + NamespaceID: uid.Test(), + Identifier: "test", + Limit: 10, + Duration: 0, + Async: false, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: time.Time{}, + }) + + route := handler.New(nil) + + h.Register(route) + + req := handler.Request{ + NamespaceId: util.Pointer(""), + NamespaceName: nil, + Identifier: "", + Limit: 10, + Duration: 1000, + } + res := testutil.CallRoute[handler.Request, handler.Response](h, route, nil, req) + + require.Equal(t, 200, res.Status) + require.NotEqual(t, "", res.Body.OverrideId) +} diff --git a/go/pkg/zen/server.go b/go/pkg/zen/server.go index 7cb7a8afaf..ec4d862a32 100644 --- a/go/pkg/zen/server.go +++ b/go/pkg/zen/server.go @@ -94,6 +94,13 @@ func (s *Server) returnSession(session any) { s.sessions.Put(session) } +// Mux returns the underlying http.ServeMux. +// +// Usually you don't need to use this, but it's here for tests. +func (s *Server) Mux() *http.ServeMux { + return s.mux +} + // Calling this function multiple times will have no effect. func (s *Server) Listen(ctx context.Context, addr string) error { s.mu.Lock() diff --git a/go/pkg/zen/services.go b/go/pkg/zen/services.go index c40f2fdb39..44eb18cc12 100644 --- a/go/pkg/zen/services.go +++ b/go/pkg/zen/services.go @@ -1,6 +1,7 @@ package zen import ( + "github.com/unkeyed/unkey/go/internal/services/keys" "github.com/unkeyed/unkey/go/pkg/clickhouse/schema" "github.com/unkeyed/unkey/go/pkg/database" "github.com/unkeyed/unkey/go/pkg/logging" @@ -14,4 +15,5 @@ type Services struct { Logger logging.Logger Database database.Database EventBuffer EventBuffer + Keys keys.KeyService }