Skip to content

Commit

Permalink
⚙️ #40: Added config validation (#98)
Browse files Browse the repository at this point in the history
- Made sure it's not possible to override the required fields by nil values accidentally by misformatting the config file
- used github.com/go-playground/validator/v10 for config validation
- created custom validation errors
- Made sure router and model IDs are unique
- Made sure there is at least one lang router configured
- Made sure that each router has at least one model configured
- Warn users in case there is only one model in the router's pool
  • Loading branch information
roma-glushko authored Jan 22, 2024
1 parent 3e2f538 commit 750fd9e
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 41 deletions.
6 changes: 1 addition & 5 deletions config.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ telemetry:
level: debug # debug, info, warn, error, fatal
encoding: console

#api:
# http:
# ...

routers:
language:
- id: myrouter
Expand All @@ -18,4 +14,4 @@ routers:
azureopenai:
api_key: ""
model: ""
base_url: ""
base_url: ""
14 changes: 12 additions & 2 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ const docTemplate = `{
"providers.LangModelConfig": {
"type": "object",
"required": [
"enabled",
"id"
],
"properties": {
Expand Down Expand Up @@ -539,7 +540,12 @@ const docTemplate = `{
"$ref": "#/definitions/octoml.Config"
},
"openai": {
"$ref": "#/definitions/openai.Config"
"description": "Add other providers like",
"allOf": [
{
"$ref": "#/definitions/openai.Config"
}
]
},
"weight": {
"type": "integer"
Expand All @@ -566,8 +572,11 @@ const docTemplate = `{
"routers.LangRouterConfig": {
"type": "object",
"required": [
"enabled",
"models",
"routers"
"retry",
"routers",
"strategy"
],
"properties": {
"enabled": {
Expand All @@ -577,6 +586,7 @@ const docTemplate = `{
"models": {
"description": "the list of models that could handle requests",
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/providers.LangModelConfig"
}
Expand Down
14 changes: 12 additions & 2 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@
"providers.LangModelConfig": {
"type": "object",
"required": [
"enabled",
"id"
],
"properties": {
Expand Down Expand Up @@ -536,7 +537,12 @@
"$ref": "#/definitions/octoml.Config"
},
"openai": {
"$ref": "#/definitions/openai.Config"
"description": "Add other providers like",
"allOf": [
{
"$ref": "#/definitions/openai.Config"
}
]
},
"weight": {
"type": "integer"
Expand All @@ -563,8 +569,11 @@
"routers.LangRouterConfig": {
"type": "object",
"required": [
"enabled",
"models",
"routers"
"retry",
"routers",
"strategy"
],
"properties": {
"enabled": {
Expand All @@ -574,6 +583,7 @@
"models": {
"description": "the list of models that could handle requests",
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/providers.LangModelConfig"
}
Expand Down
9 changes: 8 additions & 1 deletion docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,13 @@ definitions:
octoml:
$ref: '#/definitions/octoml.Config'
openai:
$ref: '#/definitions/openai.Config'
allOf:
- $ref: '#/definitions/openai.Config'
description: Add other providers like
weight:
type: integer
required:
- enabled
- id
type: object
retry.ExpRetryConfig:
Expand All @@ -305,6 +308,7 @@ definitions:
description: the list of models that could handle requests
items:
$ref: '#/definitions/providers.LangModelConfig'
minItems: 1
type: array
retry:
allOf:
Expand All @@ -317,8 +321,11 @@ definitions:
description: strategy on picking the next model to serve the request
type: string
required:
- enabled
- models
- retry
- routers
- strategy
type: object
schemas.ChatMessage:
properties:
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.21.5

require (
github.com/cloudwego/hertz v0.7.3
github.com/go-playground/validator/v10 v10.17.0
github.com/hertz-contrib/logger/zap v1.1.0
github.com/hertz-contrib/swagger v0.1.0
github.com/spf13/cobra v1.8.0
Expand All @@ -28,14 +29,18 @@ require (
github.com/cloudwego/netpoll v0.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.13 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/nyaruka/phonenumbers v1.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand All @@ -45,6 +50,7 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
Expand All @@ -43,6 +45,14 @@ github.com/go-openapi/spec v0.20.13 h1:XJDIN+dLH6vqXgafnl5SUIMnzaChQ6QTo0/UPMbkI
github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
Expand Down Expand Up @@ -71,6 +81,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
Expand Down Expand Up @@ -99,6 +111,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
Expand Down Expand Up @@ -129,6 +142,8 @@ golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ func main() {
cli := cmd.NewCLI()

if err := cli.Execute(); err != nil {
log.Fatalf("glide run finished with error: %v", err)
log.Fatalf("💥Glide has finished with error: %v", err)
}
}
2 changes: 1 addition & 1 deletion pkg/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "glide/pkg/api/http"

// Config defines configuration for all API types we support (e.g. HTTP, gRPC)
type Config struct {
HTTP *http.ServerConfig `yaml:"http"`
HTTP *http.ServerConfig `yaml:"http" validate:"required"`
}

func DefaultConfig() *Config {
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func NewCLI() *cobra.Command {

return gateway.Run(cmd.Context())
},
// SilenceUsage: true,
SilenceUsage: true,
SilenceErrors: true,
}

cli.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (

// Config is a general top-level Glide configuration
type Config struct {
Telemetry *telemetry.Config `yaml:"telemetry"`
API *api.Config `yaml:"api"`
Telemetry *telemetry.Config `yaml:"telemetry" validate:"required"`
API *api.Config `yaml:"api" validate:"required"`
Routers routers.Config `yaml:"routers" validate:"required"`
}

Expand Down
90 changes: 85 additions & 5 deletions pkg/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,39 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"

"github.com/go-playground/validator/v10"

"gopkg.in/yaml.v3"
)

// Provider reads, collects, validates and process config files
type Provider struct {
expander *Expander
Config *Config
expander *Expander
Config *Config
validator *validator.Validate
}

// NewProvider creates a instance of Config Provider
func NewProvider() *Provider {
configValidator := validator.New(validator.WithRequiredStructEnabled())

configValidator.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("yaml"), ",", 2)[0]

if name == "-" {
return ""
}

return name
})

return &Provider{
expander: &Expander{},
Config: nil,
expander: &Expander{},
Config: nil,
validator: configValidator,
}
}

Expand All @@ -38,16 +56,78 @@ func (p *Provider) Load(configPath string) (*Provider, error) {
return p, fmt.Errorf("unable to parse config file %v: %w", configPath, err)
}

// TODO: validate config values
err = p.validator.Struct(cfg)

if err != nil {
return p, p.formatValidationError(configPath, err)
}

p.Config = cfg

return p, nil
}

func (p *Provider) formatValidationError(configPath string, err error) error {
// this check is only needed when your code could produce
// an invalid value for validation such as interface with nil
// value most including myself do not usually have code like this.
if _, ok := err.(*validator.InvalidValidationError); ok {
return fmt.Errorf("invalid config file %v: %v", configPath, err)
}

errors := make([]string, 0, len(err.(validator.ValidationErrors)))

for _, fieldErr := range err.(validator.ValidationErrors) {
errors = append(
errors,
fmt.Sprintf(
"- ❌ %v", p.formatFieldError(fieldErr),
),
)
}

// from here you can create your own error messages in whatever language you wish
return fmt.Errorf(
"invalid config file %v:\n%v\nPlease make sure the config file is properly formatted",
configPath,
strings.Join(errors, "\n"),
)
}

func (p *Provider) formatFieldError(fieldErr validator.FieldError) string {
namespace := strings.TrimLeft(fieldErr.Namespace(), "Config.")

switch fieldErr.Tag() {
case "required":
return fmt.Sprintf(
"\"%v\"field is required, \"%v\" provided",
namespace,
fieldErr.Value(),
)
case "min":
if fieldErr.Kind() == reflect.Map || fieldErr.Kind() == reflect.Slice {
return fmt.Sprintf("\"%v\" field must have at least %s element(s)", namespace, fieldErr.Param())
}

return fmt.Sprintf("\"%v\" field must have minimum value: %q", namespace, fieldErr.Param())
default:
return fmt.Sprintf(
"\"%v\"field: %v",
namespace,
fieldErr.Tag(),
)
}
}

func (p *Provider) Get() *Config {
return p.Config
}

func (p *Provider) GetStr() string {
loadedConfig, _ := yaml.Marshal(p.Config)

return string(loadedConfig)
}

func (p *Provider) Start() {
}
Loading

0 comments on commit 750fd9e

Please sign in to comment.