Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add GetFullSchema and FillDefaults methods to plugins #114

Merged
merged 8 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (s *PluginService) GetFullSchema(ctx context.Context,
func (s *PluginService) GetPluginSchema(ctx context.Context,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should remain as-is: it's more descriptive of the difference between this and GetSchema, and we already know this is plugin stuff since it's part of the plugin service.

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
GGabriele marked this conversation as resolved.
Show resolved Hide resolved
//
// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GGabriele I made a mistake in the review.
This method interface is misleading and wrong.
The code takes in a plugin struct and then mutates it.
But the method signature also returns a plugin resource.

Either of the following are fine (but together not):

  • Return only an error, and document that the plugin is filled and mutated
  • Inside the function, make a copy of the input plugin, mutate the copy and return it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, my bad, sorry :(

#116

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
}