Files
netbird/shared/management/http/apiv1alpha1/runtime_tooling.go
T
dmitri-netbird 24cb7b75c2 [management] switch to libopenapi for managing of openapi-based api (#8056)
* use libopenapi OpenAPI generator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added a handler for v1alpha1/peers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wire up request validator

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* wired up spec-based validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use sync validation; set base url to v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* moved stuff around, extracted runtime libopenapu deps into runtime_tooling

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* testing /peers path params

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* fixed tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* added user schemas and paths

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use api validation in tests

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* post-merge fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* deleted tmp command used to test validator integration

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* require request body in post/put requests in order to force validation

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* making linter happy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* switch to api/v1alpha1

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* go mod tidy

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* keep the original order of middleware: metrics, cors, then auth

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* return 422 on validation errors

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* v1alpha1 spec cleanups

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* no need for a double-pointer

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* more linter fixes

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* removed more double pointers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* use di to inject api_v0 and api_v1 http routers

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* do not re-add middleware on repeated call to ApiHandler

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

* calling ApiRouter() now also calls ApiV1Router()

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>

---------

Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
2026-10-07 19:07:31 +02:00

94 lines
2.8 KiB
Go

package apiv1alpha1
import (
_ "embed"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
"github.com/netbirdio/netbird/shared/management/http/util"
"github.com/netbirdio/netbird/shared/management/status"
"github.com/pb33f/libopenapi"
validator "github.com/pb33f/libopenapi-validator"
"github.com/pb33f/libopenapi-validator/config"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi/datamodel"
log "github.com/sirupsen/logrus"
)
// TODO (dmitri) this needs to be extracted, as it will grow to 500Kb
//
//go:embed bundle.yaml
var bundle []byte
func CreateV1ApiValidatingMiddleware() (*V1ValidatorMiddleware, error) {
doc, err := libopenapi.NewDocumentWithConfiguration(bundle, &datamodel.DocumentConfiguration{
Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})),
})
if err != nil {
return nil, err
}
model, err := doc.BuildV3Model()
if err != nil {
return nil, err
}
// TODO figure out document validation: rn it's possible to have a spec
// that's not entirely correct -- parts of it fail to parse, but silently
v := validator.NewValidatorFromV3Model(&model.Model,
config.WithoutSecurityValidation(),
config.WithStandardBodyDecoders(),
config.WithRejectUnsupportedBodyContent(),
config.WithRequestDefaults())
// v.SetDocument(doc)
// v.ValidatePathParams()
// if valid, errs := v.ValidateDocument(); !valid {
// return nil, fmt.Errorf("error validating OpenAPI doc, %s", errs)
// }
return &V1ValidatorMiddleware{Validator: v}, nil
}
type V1ValidatorMiddleware struct {
Validator validator.Validator
}
func (v *V1ValidatorMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
valid, errs := v.Validator.ValidateHttpRequestSync(r)
if !valid {
validationErrs := make([]string, 0, len(errs))
for _, err := range errs {
validationErrs = append(validationErrs, validationError(err))
}
log.WithContext(r.Context()).Debugf("error validating request: %s", strings.Join(validationErrs, ", "))
util.WriteError(r.Context(), status.Errorf(status.InvalidArgument, "invalid request: %s", strings.Join(validationErrs, ", ")), w)
return
}
h.ServeHTTP(w, r)
})
}
func validationError(err *errors.ValidationError) string {
if err.SchemaValidationErrors != nil {
errs := make([]string, 0, len(err.SchemaValidationErrors))
for _, e := range err.SchemaValidationErrors {
errs = append(errs, fmt.Sprintf("field %s: %s", e.FieldPath, e.Reason))
}
return fmt.Sprintf("%s: %s", err.Message, strings.Join(errs, ", "))
} else {
if err.SpecLine > 0 && err.SpecCol > 0 {
return fmt.Sprintf("%s, Line: %d, Column: %d", err.Message, err.SpecLine, err.SpecCol)
} else {
return fmt.Sprint(err.Message)
}
}
}