From cdf409788b43670476d7ad57b3c6c87b9156f122 Mon Sep 17 00:00:00 2001 From: Harry Bagdi Date: Thu, 27 May 2021 11:50:51 -0700 Subject: [PATCH] feat: implement dynamic defaulting feature - file/codegen/main.go and corresponding schema change This is to relax the schema of kong entities that are used as input for defaults. The relaxation is necessary as otherwise the user will have to specify a name in default values. This doesn't have any side-effects because the *kong.Type is not used anywhere else in Content. - file/builder.go file/reader.go - defaulter instantiation has been moved inside builder, this feels natural, defaulter being instantiated outside the builder seems odd and the only explanation is that I simply didn't think through when implementing defaulter - other changes override the default values that are registered Fix #89 --- file/builder.go | 10 ++ file/builder_test.go | 309 +++++++++++++++++++++++++++++++++- file/codegen/main.go | 8 +- file/reader.go | 42 +++-- file/schema.go | 101 ++++++----- file/types.go | 12 +- file/zz_generated.deepcopy.go | 37 ++++ 7 files changed, 453 insertions(+), 66 deletions(-) diff --git a/file/builder.go b/file/builder.go index a11c85435..6a74524a2 100644 --- a/file/builder.go +++ b/file/builder.go @@ -47,6 +47,16 @@ func (b *stateBuilder) build() (*utils.KongRawState, *utils.KonnectRawState, err return nil, nil, err } + // defaulter + var kongDefaults KongDefaults + if b.targetContent.Info != nil { + kongDefaults = b.targetContent.Info.Defaults + } + b.defaulter, err = defaulter(kongDefaults) + if err != nil { + return nil, nil, fmt.Errorf("creating defaulter: %w", err) + } + // build b.certificates() b.caCertificates() diff --git a/file/builder_test.go b/file/builder_test.go index 71b4850d3..7788ec78b 100644 --- a/file/builder_test.go +++ b/file/builder_test.go @@ -372,8 +372,6 @@ func Test_stateBuilder_services(t *testing.T) { targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() - b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) }) @@ -1845,7 +1843,6 @@ func Test_stateBuilder_documents(t *testing.T) { func Test_stateBuilder(t *testing.T) { assert := assert.New(t) - rand.Seed(42) type fields struct { targetContent *Content currentState *state.KongState @@ -2091,9 +2088,315 @@ func Test_stateBuilder(t *testing.T) { }, }, }, + { + name: "entities with configurable defaults", + fields: fields{ + targetContent: &Content{ + Info: &Info{ + SelectorTags: []string{"tag1"}, + Defaults: KongDefaults{ + Route: &kong.Route{ + PathHandling: kong.String("v0"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + RequestBuffering: kong.Bool(false), + }, + Service: &kong.Service{ + Port: kong.Int(443), + Protocol: kong.String("https"), + ConnectTimeout: kong.Int(5000), + WriteTimeout: kong.Int(5000), + ReadTimeout: kong.Int(5000), + }, + Upstream: &kong.Upstream{ + Slots: kong.Int(100), + Healthchecks: &kong.Healthcheck{ + Active: &kong.ActiveHealthcheck{ + Concurrency: kong.Int(5), + Healthy: &kong.Healthy{ + HTTPStatuses: []int{200, 302}, + Interval: kong.Int(0), + Successes: kong.Int(0), + }, + HTTPPath: kong.String("/"), + Type: kong.String("http"), + Timeout: kong.Int(1), + Unhealthy: &kong.Unhealthy{ + HTTPFailures: kong.Int(0), + TCPFailures: kong.Int(0), + Timeouts: kong.Int(0), + Interval: kong.Int(0), + HTTPStatuses: []int{429, 404, 500, 501, 502, 503, 504, 505}, + }, + }, + Passive: &kong.PassiveHealthcheck{ + Healthy: &kong.Healthy{ + HTTPStatuses: []int{ + 200, 201, 202, 203, 204, 205, + 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, + 306, 307, 308, + }, + Successes: kong.Int(0), + }, + Unhealthy: &kong.Unhealthy{ + HTTPFailures: kong.Int(0), + TCPFailures: kong.Int(0), + Timeouts: kong.Int(0), + HTTPStatuses: []int{429, 500, 503}, + }, + }, + }, + HashOn: kong.String("none"), + HashFallback: kong.String("none"), + HashOnCookiePath: kong.String("/"), + }, + }, + }, + Services: []FService{ + { + Service: kong.Service{ + Name: kong.String("foo-service"), + }, + Routes: []*FRoute{ + { + Route: kong.Route{ + Name: kong.String("foo-route1"), + }, + }, + { + Route: kong.Route{ + ID: kong.String("d125e79a-297c-414b-bc00-ad3a87be6c2b"), + Name: kong.String("foo-route2"), + }, + }, + }, + }, + { + Service: kong.Service{ + Name: kong.String("bar-service"), + }, + Routes: []*FRoute{ + { + Route: kong.Route{ + Name: kong.String("bar-route1"), + }, + }, + { + Route: kong.Route{ + Name: kong.String("bar-route2"), + }, + }, + }, + }, + { + Service: kong.Service{ + Name: kong.String("large-payload-service"), + }, + Routes: []*FRoute{ + { + Route: kong.Route{ + Name: kong.String("dont-buffer-these"), + RequestBuffering: kong.Bool(false), + ResponseBuffering: kong.Bool(false), + }, + }, + { + Route: kong.Route{ + Name: kong.String("buffer-these"), + RequestBuffering: kong.Bool(true), + ResponseBuffering: kong.Bool(true), + }, + }, + }, + }, + }, + Upstreams: []FUpstream{ + { + Upstream: kong.Upstream{ + Name: kong.String("foo"), + Slots: kong.Int(42), + }, + }, + }, + }, + currentState: existingServiceState(), + }, + want: &utils.KongRawState{ + Services: []*kong.Service{ + { + ID: kong.String("538c7f96-b164-4f1b-97bb-9f4bb472e89f"), + Name: kong.String("foo-service"), + Port: kong.Int(443), + Protocol: kong.String("https"), + ConnectTimeout: kong.Int(5000), + WriteTimeout: kong.Int(5000), + ReadTimeout: kong.Int(5000), + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("dfd79b4d-7642-4b61-ba0c-9f9f0d3ba55b"), + Name: kong.String("bar-service"), + Port: kong.Int(443), + Protocol: kong.String("https"), + ConnectTimeout: kong.Int(5000), + WriteTimeout: kong.Int(5000), + ReadTimeout: kong.Int(5000), + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("9e6f82e5-4e74-4e81-a79e-4bbd6fe34cdc"), + Name: kong.String("large-payload-service"), + Port: kong.Int(443), + Protocol: kong.String("https"), + ConnectTimeout: kong.Int(5000), + WriteTimeout: kong.Int(5000), + ReadTimeout: kong.Int(5000), + Tags: kong.StringSlice("tag1"), + }, + }, + Routes: []*kong.Route{ + { + ID: kong.String("5b1484f2-5209-49d9-b43e-92ba09dd9d52"), + Name: kong.String("foo-route1"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + RequestBuffering: kong.Bool(false), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("538c7f96-b164-4f1b-97bb-9f4bb472e89f"), + }, + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("d125e79a-297c-414b-bc00-ad3a87be6c2b"), + Name: kong.String("foo-route2"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + RequestBuffering: kong.Bool(false), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("538c7f96-b164-4f1b-97bb-9f4bb472e89f"), + }, + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("0cc0d614-4c88-4535-841a-cbe0709b0758"), + Name: kong.String("bar-route1"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + RequestBuffering: kong.Bool(false), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("dfd79b4d-7642-4b61-ba0c-9f9f0d3ba55b"), + }, + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("083f61d3-75bc-42b4-9df4-f91929e18fda"), + Name: kong.String("bar-route2"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + RequestBuffering: kong.Bool(false), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("dfd79b4d-7642-4b61-ba0c-9f9f0d3ba55b"), + }, + Tags: kong.StringSlice("tag1"), + }, + { + ID: kong.String("ba843ee8-d63e-4c4f-be1c-ebea546d8fac"), + Name: kong.String("dont-buffer-these"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("9e6f82e5-4e74-4e81-a79e-4bbd6fe34cdc"), + }, + Tags: kong.StringSlice("tag1"), + RequestBuffering: kong.Bool(false), + ResponseBuffering: kong.Bool(false), + }, + { + ID: kong.String("13dd1aac-04ce-4ea2-877c-5579cfa2c78e"), + Name: kong.String("buffer-these"), + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + PathHandling: kong.String("v0"), + Service: &kong.Service{ + ID: kong.String("9e6f82e5-4e74-4e81-a79e-4bbd6fe34cdc"), + }, + Tags: kong.StringSlice("tag1"), + RequestBuffering: kong.Bool(true), + ResponseBuffering: kong.Bool(true), + }, + }, + Upstreams: []*kong.Upstream{ + { + ID: kong.String("1b0bafae-881b-42a7-9110-8a42ed3c903c"), + Name: kong.String("foo"), + Slots: kong.Int(42), + Healthchecks: &kong.Healthcheck{ + Active: &kong.ActiveHealthcheck{ + Concurrency: kong.Int(5), + Healthy: &kong.Healthy{ + HTTPStatuses: []int{200, 302}, + Interval: kong.Int(0), + Successes: kong.Int(0), + }, + HTTPPath: kong.String("/"), + Type: kong.String("http"), + Timeout: kong.Int(1), + Unhealthy: &kong.Unhealthy{ + HTTPFailures: kong.Int(0), + TCPFailures: kong.Int(0), + Timeouts: kong.Int(0), + Interval: kong.Int(0), + HTTPStatuses: []int{429, 404, 500, 501, 502, 503, 504, 505}, + }, + }, + Passive: &kong.PassiveHealthcheck{ + Healthy: &kong.Healthy{ + HTTPStatuses: []int{ + 200, 201, 202, 203, 204, 205, + 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, + 306, 307, 308, + }, + Successes: kong.Int(0), + }, + Unhealthy: &kong.Unhealthy{ + HTTPFailures: kong.Int(0), + TCPFailures: kong.Int(0), + Timeouts: kong.Int(0), + HTTPStatuses: []int{429, 500, 503}, + }, + }, + }, + HashOn: kong.String("none"), + HashFallback: kong.String("none"), + HashOnCookiePath: kong.String("/"), + Tags: kong.StringSlice("tag1"), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + rand.Seed(42) b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, diff --git a/file/codegen/main.go b/file/codegen/main.go index f39abf0a6..44d3593d9 100644 --- a/file/codegen/main.go +++ b/file/codegen/main.go @@ -58,16 +58,16 @@ func main() { return nil } schema := reflector.Reflect(file.Content{}) - schema.Definitions["Service"].AnyOf = anyOfNameOrID + // schema.Definitions["Service"].AnyOf = anyOfNameOrID schema.Definitions["FService"].AnyOf = anyOfNameOrID - schema.Definitions["Route"].AnyOf = anyOfNameOrID + // schema.Definitions["Route"].AnyOf = anyOfNameOrID schema.Definitions["FRoute"].AnyOf = anyOfNameOrID - schema.Definitions["Consumer"].AnyOf = anyOfUsernameOrID + // schema.Definitions["Consumer"].AnyOf = anyOfUsernameOrID schema.Definitions["FConsumer"].AnyOf = anyOfUsernameOrID - schema.Definitions["Upstream"].Required = []string{"name"} + // schema.Definitions["Upstream"].Required = []string{"name"} schema.Definitions["FUpstream"].Required = []string{"name"} schema.Definitions["FTarget"].Required = []string{"target"} diff --git a/file/reader.go b/file/reader.go index fcf0e460a..628eec2a3 100644 --- a/file/reader.go +++ b/file/reader.go @@ -36,13 +36,6 @@ func GetForKonnect(fileContent *Content, opt RenderConfig) (*utils.KongRawState, // setup builder.targetContent = fileContent builder.currentState = opt.CurrentState - builder.kongVersion = opt.KongVersion - - d, err := utils.GetKongDefaulter() - if err != nil { - return nil, nil, fmt.Errorf("creating defaulter: %w", err) - } - builder.defaulter = d kongState, konnectState, err := builder.build() if err != nil { @@ -51,6 +44,34 @@ func GetForKonnect(fileContent *Content, opt RenderConfig) (*utils.KongRawState, return kongState, konnectState, nil } +func defaulter(defaults KongDefaults) (*utils.Defaulter, error) { + d, err := utils.GetKongDefaulter() + if err != nil { + return nil, err + } + if defaults.Route != nil { + if err = d.Register(defaults.Route); err != nil { + return nil, err + } + } + if defaults.Service != nil { + if err = d.Register(defaults.Service); err != nil { + return nil, err + } + } + if defaults.Upstream != nil { + if err = d.Register(defaults.Upstream); err != nil { + return nil, err + } + } + if defaults.Target != nil { + if err = d.Register(defaults.Target); err != nil { + return nil, err + } + } + return d, nil +} + // Get process the fileContent and renders a RawState. // IDs of entities are matches based on currentState. func Get(fileContent *Content, opt RenderConfig) (*utils.KongRawState, error) { @@ -58,13 +79,6 @@ func Get(fileContent *Content, opt RenderConfig) (*utils.KongRawState, error) { // setup builder.targetContent = fileContent builder.currentState = opt.CurrentState - builder.kongVersion = opt.KongVersion - - d, err := utils.GetKongDefaulter() - if err != nil { - return nil, fmt.Errorf("creating defaulter: %w", err) - } - builder.defaulter = d state, _, err := builder.build() if err != nil { diff --git a/file/schema.go b/file/schema.go index 9ffa8fdf9..4e3ef8c51 100644 --- a/file/schema.go +++ b/file/schema.go @@ -263,19 +263,7 @@ const contentSchema = `{ } }, "additionalProperties": false, - "type": "object", - "anyOf": [ - { - "required": [ - "username" - ] - }, - { - "required": [ - "id" - ] - } - ] + "type": "object" }, "FCACertificate": { "required": [ @@ -643,7 +631,6 @@ const contentSchema = `{ "type": "boolean" }, "service": { - "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Service" }, "snis": { @@ -654,7 +641,6 @@ const contentSchema = `{ }, "sources": { "items": { - "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/CIDRPort" }, "type": "array" @@ -850,7 +836,6 @@ const contentSchema = `{ "type": "string" }, "upstream": { - "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Upstream" }, "weight": { @@ -893,7 +878,6 @@ const contentSchema = `{ "type": "string" }, "healthchecks": { - "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Healthcheck" }, "host_header": { @@ -1010,6 +994,10 @@ const contentSchema = `{ }, "Info": { "properties": { + "defaults": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/KongDefaults" + }, "select_tags": { "items": { "type": "string" @@ -1097,6 +1085,28 @@ const contentSchema = `{ "additionalProperties": false, "type": "object" }, + "KongDefaults": { + "properties": { + "route": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Route" + }, + "service": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Service" + }, + "target": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Target" + }, + "upstream": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/Upstream" + } + }, + "additionalProperties": false, + "type": "object" + }, "MTLSAuth": { "required": [ "id", @@ -1288,6 +1298,7 @@ const contentSchema = `{ }, "sources": { "items": { + "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/CIDRPort" }, "type": "array" @@ -1306,19 +1317,7 @@ const contentSchema = `{ } }, "additionalProperties": false, - "type": "object", - "anyOf": [ - { - "required": [ - "name" - ] - }, - { - "required": [ - "id" - ] - } - ] + "type": "object" }, "SNI": { "properties": { @@ -1353,6 +1352,7 @@ const contentSchema = `{ "type": "array" }, "client_certificate": { + "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Certificate" }, "connect_timeout": { @@ -1408,19 +1408,34 @@ const contentSchema = `{ } }, "additionalProperties": false, - "type": "object", - "anyOf": [ - { - "required": [ - "name" - ] + "type": "object" + }, + "Target": { + "properties": { + "created_at": { + "type": "number" }, - { - "required": [ - "id" - ] + "id": { + "type": "string" + }, + "tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "target": { + "type": "string" + }, + "upstream": { + "$ref": "#/definitions/Upstream" + }, + "weight": { + "type": "integer" } - ] + }, + "additionalProperties": false, + "type": "object" }, "Unhealthy": { "properties": { @@ -1447,9 +1462,6 @@ const contentSchema = `{ "type": "object" }, "Upstream": { - "required": [ - "name" - ], "properties": { "algorithm": { "type": "string" @@ -1479,6 +1491,7 @@ const contentSchema = `{ "type": "string" }, "healthchecks": { + "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/Healthcheck" }, "host_header": { diff --git a/file/types.go b/file/types.go index 584f2d6cb..ff40ea485 100644 --- a/file/types.go +++ b/file/types.go @@ -506,10 +506,20 @@ type FRBACEndpointPermission struct { kong.RBACEndpointPermission `yaml:",inline,omitempty"` } +// KongDefaults represents default values that are filled in +// for entities with corresponding missing properties. +type KongDefaults struct { + Service *kong.Service `json:"service,omitempty" yaml:"service,omitempty"` + Route *kong.Route `json:"route,omitempty" yaml:"route,omitempty"` + Upstream *kong.Upstream `json:"upstream,omitempty" yaml:"upstream,omitempty"` + Target *kong.Target `json:"target,omitempty" yaml:"target,omitempty"` +} + // Info contains meta-data of the file. // +k8s:deepcopy-gen=true type Info struct { - SelectorTags []string `json:"select_tags,omitempty" yaml:"select_tags,omitempty"` + SelectorTags []string `json:"select_tags,omitempty" yaml:"select_tags,omitempty"` + Defaults KongDefaults `json:"defaults,omitempty" yaml:"defaults,omitempty"` } // +k8s:deepcopy-gen=true diff --git a/file/zz_generated.deepcopy.go b/file/zz_generated.deepcopy.go index ff03faf13..68aafc0b1 100644 --- a/file/zz_generated.deepcopy.go +++ b/file/zz_generated.deepcopy.go @@ -619,6 +619,7 @@ func (in *Info) DeepCopyInto(out *Info) { *out = make([]string, len(*in)) copy(*out, *in) } + in.Defaults.DeepCopyInto(&out.Defaults) return } @@ -652,3 +653,39 @@ func (in *Kong) DeepCopy() *Kong { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KongDefaults) DeepCopyInto(out *KongDefaults) { + *out = *in + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(kong.Service) + (*in).DeepCopyInto(*out) + } + if in.Route != nil { + in, out := &in.Route, &out.Route + *out = new(kong.Route) + (*in).DeepCopyInto(*out) + } + if in.Upstream != nil { + in, out := &in.Upstream, &out.Upstream + *out = new(kong.Upstream) + (*in).DeepCopyInto(*out) + } + if in.Target != nil { + in, out := &in.Target, &out.Target + *out = new(kong.Target) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KongDefaults. +func (in *KongDefaults) DeepCopy() *KongDefaults { + if in == nil { + return nil + } + out := new(KongDefaults) + in.DeepCopyInto(out) + return out +}