Skip to content

Commit

Permalink
feat: add GetFullSchema and FillDefaults methods to plugins (#114)
Browse files Browse the repository at this point in the history
GetFullSchema: returns full schema for plugins (/schemas/plugins/<name>)
FillDefaults: given a schema and a config object, fills all defaults
  • Loading branch information
GGabriele committed Jan 21, 2022
1 parent 8d08c5d commit bfe90b4
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 3 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ go 1.16

require (
github.com/blang/semver/v4 v4.0.0
github.com/google/go-cmp v0.5.7
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.3.0
github.com/mitchellh/mapstructure v1.4.3
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.13.0
k8s.io/code-generator v0.22.4
)
9 changes: 8 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand Down Expand Up @@ -116,6 +117,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
Expand Down
30 changes: 28 additions & 2 deletions kong/plugin_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,40 @@ type AbstractPluginService interface {
ListAllForRoute(ctx context.Context, routeID *string) ([]*Plugin, error)
// Validate validates a Plugin against its schema
Validate(ctx context.Context, plugin *Plugin) (bool, string, error)
// GetSchema retrieves the schema of a plugin
// GetSchema retrieves the config schema of a plugin.
//
// Deprecated: Use GetFullSchema instead.
GetSchema(ctx context.Context, pluginName *string) (map[string]interface{}, error)
// GetFullSchema retrieves the full schema of a plugin.
// This makes the use of `/schemas` endpoint in Kong.
GetFullSchema(ctx context.Context, pluginName *string) (map[string]interface{}, error)
}

// PluginService handles Plugins in Kong.
type PluginService service

// GetSchema retrieves the schema of a plugin
// GetFullSchema retrieves the full schema of a plugin.
func (s *PluginService) GetFullSchema(ctx context.Context,
pluginName *string) (map[string]interface{}, error) {
if isEmptyString(pluginName) {
return nil, fmt.Errorf("pluginName cannot be empty")
}
endpoint := fmt.Sprintf("/schemas/plugins/%v", *pluginName)
req, err := s.client.NewRequest("GET", endpoint, nil, nil)
if err != nil {
return nil, err
}
var schema map[string]interface{}
_, err = s.client.Do(ctx, req, &schema)
if err != nil {
return nil, err
}
return schema, nil
}

// GetSchema retrieves the config schema of a plugin
//
// Deprecated: Use GetPluginSchema instead
func (s *PluginService) GetSchema(ctx context.Context,
pluginName *string) (map[string]interface{}, error) {
if isEmptyString(pluginName) {
Expand Down
131 changes: 131 additions & 0 deletions kong/plugin_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kong
import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -320,6 +321,136 @@ func TestPluginListAllForEntityEndpoint(T *testing.T) {
assert.Nil(client.Services.Delete(defaultCtx, createdService.ID))
}

func TestGetFullSchema(T *testing.T) {
assert := assert.New(T)

client, err := NewTestClient(nil, nil)
assert.Nil(err)
assert.NotNil(client)

schema, err := client.Plugins.GetFullSchema(defaultCtx, String("key-auth"))
_, ok := schema["fields"]
assert.True(ok)
assert.Nil(err)

schema, err = client.Plugins.GetFullSchema(defaultCtx, String("noexist"))
assert.Nil(schema)
assert.NotNil(err)
assert.True(IsNotFoundErr(err))
}

func TestFillPluginDefaults(T *testing.T) {
assert := assert.New(T)

client, err := NewTestClient(nil, nil)
assert.Nil(err)
assert.NotNil(client)

tests := []struct {
name string
plugin *Plugin
expected *Plugin
}{
{
name: "no config no protocols",
plugin: &Plugin{
Name: String("basic-auth"),
},
expected: &Plugin{
Name: String("basic-auth"),
Config: Configuration{
"anonymous": nil,
"hide_credentials": false,
},
Protocols: []*string{String("grpc"), String("grpcs"), String("http"), String("https")},
Enabled: Bool(true),
},
},
{
name: "partial config no protocols",
plugin: &Plugin{
Name: String("basic-auth"),
Config: Configuration{
"hide_credentials": true,
},
},
expected: &Plugin{
Name: String("basic-auth"),
Config: Configuration{
"anonymous": nil,
"hide_credentials": true,
},
Protocols: []*string{String("grpc"), String("grpcs"), String("http"), String("https")},
Enabled: Bool(true),
},
},
{
name: "nested config partial protocols",
plugin: &Plugin{
Name: String("request-transformer"),
Config: Configuration{
"add": map[string]interface{}{
"body": []interface{}{},
"headers": "x-new-header:value",
"querystring": []interface{}{},
},
},
Enabled: Bool(false),
Protocols: []*string{String("grpc"), String("grpcs")},
},
expected: &Plugin{
Name: String("request-transformer"),
Config: Configuration{
"http_method": nil,
"add": map[string]interface{}{
"body": []interface{}{},
"headers": "x-new-header:value",
"querystring": []interface{}{},
},
"append": map[string]interface{}{
"body": []interface{}{},
"headers": []interface{}{},
"querystring": []interface{}{},
},
"remove": map[string]interface{}{
"body": []interface{}{},
"headers": []interface{}{},
"querystring": []interface{}{},
},
"rename": map[string]interface{}{
"body": []interface{}{},
"headers": []interface{}{},
"querystring": []interface{}{},
},
"replace": map[string]interface{}{
"body": []interface{}{},
"headers": []interface{}{},
"querystring": []interface{}{},
"uri": nil,
},
},
Protocols: []*string{String("grpc"), String("grpcs")},
Enabled: Bool(false),
},
},
}

for _, tc := range tests {
T.Run(tc.name, func(t *testing.T) {
fullSchema, err := client.Plugins.GetFullSchema(defaultCtx, tc.plugin.Name)
assert.Nil(err)
assert.NotNil(fullSchema)
got, err := FillPluginsDefaults(tc.plugin, fullSchema)
if err != nil {
t.Errorf(err.Error())
}
if diff := cmp.Diff(got, tc.expected); diff != "" {
t.Errorf(diff)
}
})
}
}

func comparePlugins(expected, actual []*Plugin) bool {
var expectedNames, actualNames []string
for _, plugin := range expected {
Expand Down
86 changes: 86 additions & 0 deletions kong/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package kong

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/blang/semver/v4"
"github.com/tidwall/gjson"
)

const (
Expand Down Expand Up @@ -141,3 +143,87 @@ func VersionFromInfo(info map[string]interface{}) string {
}
return version.(string)
}

func getDefaultProtocols(schema gjson.Result) []*string {
var res []*string
fields := schema.Get("fields")

for _, field := range fields.Array() {
protocols := field.Map()["protocols"]
d := protocols.Get("default")
if d.Exists() {
for _, v := range d.Array() {
res = append(res, String(v.String()))
}
return res
}
}
return res
}

func fillConfigRecord(schema gjson.Result, config Configuration) Configuration {
res := config.DeepCopy()
value := schema.Get("fields")

value.ForEach(func(key, value gjson.Result) bool {
// get the key name
ms := value.Map()
fname := ""
for k := range ms {
fname = k
break
}

if fname == "config" {
newConfig := fillConfigRecord(value.Get(fname), config)
res = newConfig
return true
}

// check if key is already set in the config
if _, ok := config[fname]; ok {
// yes, don't set it
return true
}
ftype := value.Get(fname + ".type")
if ftype.String() == "record" {
subConfig := config[fname]
if subConfig == nil {
subConfig = make(map[string]interface{})
}
newSubConfig := fillConfigRecord(value.Get(fname), subConfig.(map[string]interface{}))
res[fname] = map[string]interface{}(newSubConfig)
return true
}
value = value.Get(fname + ".default")
if value.Exists() {
res[fname] = value.Value()
} else {
// if no default exists, set an explicit nil
res[fname] = nil
}
return true
})

return res
}

// FillPluginsDefaults ingests plugin's defaults from its schema
func FillPluginsDefaults(plugin *Plugin, schema map[string]interface{}) (*Plugin, error) {
jsonb, err := json.Marshal(&schema)
if err != nil {
return nil, err
}
gjsonSchema := gjson.ParseBytes((jsonb))
if plugin.Config == nil {
plugin.Config = make(Configuration)
}
plugin.Config = fillConfigRecord(gjsonSchema, plugin.Config)
if plugin.Protocols == nil {
plugin.Protocols = getDefaultProtocols(gjsonSchema)
}
if plugin.Enabled == nil {
plugin.Enabled = Bool(true)
}
return plugin, nil
}

0 comments on commit bfe90b4

Please sign in to comment.