Skip to content

Commit 63d6912

Browse files
authored
refactor(plugin): validate plugin configuration based on OpenAPI schema (#269)
1 parent 6af370b commit 63d6912

File tree

31 files changed

+447
-352
lines changed

31 files changed

+447
-352
lines changed

admin/api/plugins.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ func (api *API) CreatePlugin(w http.ResponseWriter, r *http.Request) {
4444
return
4545
}
4646

47-
p, err := model.Plugin()
48-
api.assert(err)
49-
model.Config = utils.Must(p.MarshalConfig())
50-
err = api.db.PluginsWS.Insert(r.Context(), &model)
47+
err := api.db.PluginsWS.Insert(r.Context(), &model)
5148
api.assert(err)
5249

5350
api.json(201, w, model)
@@ -73,11 +70,6 @@ func (api *API) UpdatePlugin(w http.ResponseWriter, r *http.Request) {
7370
return
7471
}
7572

76-
p, err := model.Plugin()
77-
api.assert(err)
78-
79-
model.Config = utils.Must(p.MarshalConfig())
80-
8173
model.ID = id
8274
err = api.db.PluginsWS.Update(r.Context(), model)
8375
api.assert(err)

db/entities/plugin.go

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package entities
22

33
import (
4+
"database/sql/driver"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -44,11 +45,11 @@ func (m *Plugin) Validate() error {
4445
}
4546

4647
// validate plugin configuration
47-
p, err := m.Plugin()
48+
p, err := m.ToPlugin()
4849
if err != nil {
4950
return err
5051
}
51-
if err = p.ValidateConfig(); err != nil {
52+
if err = p.ValidateConfig(m.Config); err != nil {
5253
if e, ok := err.(*errs.ValidateError); ok {
5354
e.Fields = map[string]interface{}{
5455
"config": e.Fields,
@@ -57,6 +58,13 @@ func (m *Plugin) Validate() error {
5758
}
5859
return err
5960
}
61+
62+
err = p.Init(m.Config)
63+
if err != nil {
64+
return err
65+
}
66+
m.Config = p.GetConfig()
67+
6068
return nil
6169
}
6270

@@ -69,44 +77,32 @@ func (m *Plugin) UnmarshalJSON(data []byte) error {
6977
return json.Unmarshal(data, (*alias)(m))
7078
}
7179

72-
func (m *Plugin) Plugin() (plugin.Plugin, error) {
73-
r := plugin.GetRegistration(m.Name)
74-
if r == nil {
80+
func (m *Plugin) ToPlugin() (plugin.Plugin, error) {
81+
executor, ok := plugin.New(m.Name)
82+
if !ok {
7583
return nil, fmt.Errorf("unknown plugin name: '%s'", m.Name)
7684
}
77-
78-
executor, err := r.New(m.Config)
79-
if err != nil {
80-
return nil, err
81-
}
8285
return executor, nil
8386
}
8487

85-
type PluginConfiguration json.RawMessage
88+
type PluginConfiguration map[string]interface{}
8689

87-
func (m PluginConfiguration) MarshalYAML() (interface{}, error) {
88-
if len(m) == 0 {
89-
return nil, nil
90-
}
91-
data := make(map[string]interface{})
92-
err := json.Unmarshal(m, &data)
93-
if err != nil {
94-
return nil, err
95-
}
96-
return data, nil
90+
func (m *PluginConfiguration) Scan(src interface{}) error {
91+
return json.Unmarshal(src.([]byte), m)
9792
}
9893

99-
func (m PluginConfiguration) MarshalJSON() ([]byte, error) {
94+
func (m PluginConfiguration) Value() (driver.Value, error) {
10095
if m == nil {
101-
return []byte("null"), nil
96+
return []byte(`{}`), nil
10297
}
103-
return m, nil
98+
return json.Marshal(m)
10499
}
105100

106101
func (m *PluginConfiguration) UnmarshalJSON(data []byte) error {
107-
if m == nil {
108-
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
102+
v := make(map[string]interface{})
103+
if err := json.Unmarshal(data, &v); err != nil {
104+
return err
109105
}
110-
*m = append((*m)[0:0], data...)
106+
*m = v
111107
return nil
112108
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ require (
6464
github.com/mailru/easyjson v0.7.7 // indirect
6565
github.com/mattn/go-colorable v0.1.13 // indirect
6666
github.com/mattn/go-isatty v0.0.19 // indirect
67+
github.com/mitchellh/mapstructure v1.5.0 // indirect
6768
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
6869
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
6970
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
170170
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
171171
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
172172
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
173+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
174+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
173175
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
174176
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
175177
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=

openapi.yml

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ components:
936936
nullable: true
937937
config:
938938
type: object
939-
nullable: true
939+
default: { }
940940
metadata:
941941
$ref: "#/components/schemas/Metadata"
942942
created_at:
@@ -988,3 +988,64 @@ components:
988988
required:
989989
- quota
990990
- period
991+
992+
993+
WasmPluginConfiguration:
994+
description: "The wasm plugin configuration"
995+
type: object
996+
properties:
997+
file:
998+
description: "The file path to the wasm file."
999+
type: string
1000+
envs:
1001+
description: "Environments that can be used in wasm."
1002+
type: object
1003+
additionalProperties:
1004+
type: string
1005+
default: { }
1006+
required:
1007+
- file
1008+
1009+
FunctionPluginConfiguration:
1010+
description: "The function plugin configuration"
1011+
type: object
1012+
properties:
1013+
function:
1014+
description: "The function content."
1015+
type: string
1016+
maxLength: 1048576
1017+
required:
1018+
- function
1019+
1020+
WebhookxSignaturePluginConfiguration:
1021+
description: "The webhookx-signature plugin configuration"
1022+
type: object
1023+
properties:
1024+
signing_secret:
1025+
description: "The signature secret."
1026+
type: string
1027+
minLength: 1
1028+
1029+
JsonschemaValidatorPluginConfiguration:
1030+
description: "The jsonschema-validator plugin configuration"
1031+
type: object
1032+
properties:
1033+
draft:
1034+
type: string
1035+
enum: ["6"]
1036+
default: "6"
1037+
default_schema:
1038+
description: "The default schema"
1039+
type: string
1040+
format: jsonschema
1041+
maxLength: 1048576
1042+
schemas:
1043+
description: "Defines event's schema."
1044+
type: object
1045+
additionalProperties:
1046+
type: object
1047+
properties:
1048+
schema:
1049+
type: string
1050+
format: jsonschema
1051+
maxLength: 1048576

pkg/declarative/types.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ func (cfg *Configuration) Validate() error {
5454
if err := model.Validate(); err != nil {
5555
return err
5656
}
57-
p, err := model.Plugin()
58-
if err != nil {
59-
return err
60-
}
61-
model.Config = utils.Must(p.MarshalConfig())
6257
}
6358
}
6459

@@ -67,11 +62,6 @@ func (cfg *Configuration) Validate() error {
6762
if err := model.Validate(); err != nil {
6863
return err
6964
}
70-
p, err := model.Plugin()
71-
if err != nil {
72-
return err
73-
}
74-
model.Config = utils.Must(p.MarshalConfig())
7565
}
7666
}
7767

pkg/openapi/openapi.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
package openapi
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"github.com/getkin/kin-openapi/openapi3"
78
"github.com/webhookx-io/webhookx/pkg/errs"
89
"strconv"
910
)
1011

12+
type FormatValidatorFunc[T any] func(T) error
13+
14+
func (fn FormatValidatorFunc[T]) Validate(value T) error { return fn(value) }
15+
16+
func init() {
17+
openapi3.DefineStringFormatValidator("jsonschema", FormatValidatorFunc[string](func(s string) error {
18+
schema := &openapi3.Schema{}
19+
if err := schema.UnmarshalJSON([]byte(s)); err != nil {
20+
return err
21+
}
22+
if err := schema.Validate(context.TODO(), openapi3.EnableSchemaFormatValidation()); err != nil {
23+
return err
24+
}
25+
return nil
26+
}))
27+
}
28+
1129
func SetDefaults(schema *openapi3.Schema, defaults map[string]interface{}) error {
1230
data := make(map[string]interface{})
1331
_ = schema.VisitJSON(data,
@@ -122,13 +140,9 @@ func insertError(current map[string]interface{}, i int, paths []string, err *ope
122140
if isIndex {
123141
ensureArray(current, "", index)
124142
arr := current[""].([]interface{})
125-
if err.Origin == nil {
126-
arr[index] = formatError(err)
127-
}
143+
arr[index] = formatError(err)
128144
} else {
129-
if err.Origin == nil {
130-
current[key] = formatError(err)
131-
}
145+
current[key] = formatError(err)
132146
}
133147
return
134148
}

pkg/plugin/base.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"github.com/getkin/kin-openapi/openapi3"
6+
"github.com/mitchellh/mapstructure"
7+
"github.com/webhookx-io/webhookx/pkg/openapi"
8+
"github.com/webhookx-io/webhookx/utils"
9+
)
10+
11+
// Configuration plugin configuration
12+
type Configuration interface {
13+
Schema() *openapi3.Schema
14+
}
15+
16+
type BasePlugin[T Configuration] struct {
17+
Config T
18+
}
19+
20+
func (p *BasePlugin[T]) Init(config map[string]interface{}) error {
21+
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
22+
TagName: "json",
23+
Result: &p.Config,
24+
})
25+
if err != nil {
26+
return err
27+
}
28+
return decoder.Decode(config)
29+
}
30+
31+
func (p *BasePlugin[T]) GetConfig() map[string]interface{} {
32+
m, err := utils.StructToMap(p.Config)
33+
if err != nil {
34+
panic(err)
35+
}
36+
return m
37+
}
38+
39+
func (p *BasePlugin[T]) ValidateConfig(config map[string]interface{}) error {
40+
err := openapi.Validate(p.Config.Schema(), config)
41+
if err != nil {
42+
return err
43+
}
44+
return nil
45+
}
46+
47+
func (p *BasePlugin[T]) ExecuteOutbound(ctx context.Context, outbound *Outbound) error {
48+
panic("not implemented")
49+
}
50+
51+
func (p *BasePlugin[T]) ExecuteInbound(ctx context.Context, inbound *Inbound) (InboundResult, error) {
52+
panic("not implemented")
53+
}

pkg/plugin/base_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"github.com/getkin/kin-openapi/openapi3"
6+
"github.com/stretchr/testify/assert"
7+
"testing"
8+
)
9+
10+
type config struct {
11+
}
12+
13+
func (c config) Schema() *openapi3.Schema {
14+
return &openapi3.Schema{}
15+
}
16+
17+
type MyPlugin struct {
18+
BasePlugin[config]
19+
}
20+
21+
func (m MyPlugin) Name() string {
22+
panic("my-plugin")
23+
}
24+
25+
func Test(t *testing.T) {
26+
myPlugin := &MyPlugin{}
27+
assert.PanicsWithValue(t, "not implemented", func() { myPlugin.ExecuteInbound(context.TODO(), nil) })
28+
assert.PanicsWithValue(t, "not implemented", func() { myPlugin.ExecuteOutbound(context.TODO(), nil) })
29+
}

0 commit comments

Comments
 (0)