diff --git a/file/builder.go b/file/builder.go index e996d6d10..f4820051c 100644 --- a/file/builder.go +++ b/file/builder.go @@ -27,6 +27,8 @@ type stateBuilder struct { schemasCache map[string]map[string]interface{} + disableDynamicDefaults bool + err error } @@ -51,15 +53,11 @@ 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) + defaulter, err := defaulter(b.ctx, b.client, b.targetContent, b.disableDynamicDefaults) if err != nil { - return nil, nil, fmt.Errorf("creating defaulter: %w", err) + return nil, nil, err } + b.defaulter = defaulter // build b.certificates() @@ -748,6 +746,24 @@ func (b *stateBuilder) plugins() { } } +// strip_path schema default value is 'true', but it cannot be set when +// protocols include 'grpc' and/or 'grpcs'. When users explicitly set +// strip_path to 'true' with grpc/s protocols, deck returns a schema violation error. +// When strip_path is not set and protocols include grpc/s, deck sets strip_path to 'false', +// despite its default value would be 'true' under normal circumstances. +func getStripPathBasedOnProtocols(route kong.Route) (*bool, error) { + for _, p := range route.Protocols { + if *p == "grpc" || *p == "grpcs" { + if route.StripPath != nil && *route.StripPath { + return nil, fmt.Errorf("schema violation (strip_path: cannot set " + + "'strip_path' when 'protocols' is 'grpc' or 'grpcs')") + } + return kong.Bool(false), nil + } + } + return route.StripPath, nil +} + func (b *stateBuilder) ingestRoute(r FRoute) error { if utils.Empty(r.ID) { route, err := b.currentState.Routes.Get(*r.Name) @@ -761,10 +777,16 @@ func (b *stateBuilder) ingestRoute(r FRoute) error { } utils.MustMergeTags(&r, b.selectTags) + + stripPath, err := getStripPathBasedOnProtocols(r.Route) + if err != nil { + return err + } + r.Route.StripPath = stripPath b.defaulter.MustSet(&r.Route) b.rawState.Routes = append(b.rawState.Routes, &r.Route) - err := b.intermediate.Routes.Add(state.Route{Route: r.Route}) + err = b.intermediate.Routes.Add(state.Route{Route: r.Route}) if err != nil { return err } @@ -868,30 +890,21 @@ func pluginRelations(plugin *kong.Plugin) (cID, rID, sID string) { return } -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 - } +func defaulter( + ctx context.Context, client *kong.Client, fileContent *Content, disableDynamicDefaults bool, +) (*utils.Defaulter, error) { + var kongDefaults KongDefaults + if fileContent.Info != nil { + kongDefaults = fileContent.Info.Defaults } - if defaults.Upstream != nil { - if err = d.Register(defaults.Upstream); err != nil { - return nil, err - } + opts := utils.DefaulterOpts{ + Client: client, + KongDefaults: kongDefaults, + DisableDynamicDefaults: disableDynamicDefaults, } - if defaults.Target != nil { - if err = d.Register(defaults.Target); err != nil { - return nil, err - } + defaulter, err := utils.GetDefaulter(ctx, opts) + if err != nil { + return nil, fmt.Errorf("creating defaulter: %w", err) } - return d, nil + return defaulter, nil } diff --git a/file/builder_test.go b/file/builder_test.go index 3876b9e47..241c07753 100644 --- a/file/builder_test.go +++ b/file/builder_test.go @@ -1,6 +1,7 @@ package file import ( + "context" "encoding/hex" "math/rand" "os" @@ -15,8 +16,82 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + defaultPort = 80 + defaultTimeout = 60000 + defaultSlots = 10000 + defaultWeight = 100 + defaultConcurrency = 10 +) + var kong130Version = semver.MustParse("1.3.0") +var kongDefaults = KongDefaults{ + Service: &kong.Service{ + Port: kong.Int(defaultPort), + Protocol: kong.String("http"), + ConnectTimeout: kong.Int(defaultTimeout), + WriteTimeout: kong.Int(defaultTimeout), + ReadTimeout: kong.Int(defaultTimeout), + }, + Route: &kong.Route{ + PreserveHost: kong.Bool(false), + RegexPriority: kong.Int(0), + StripPath: kong.Bool(false), + Protocols: kong.StringSlice("http", "https"), + }, + Upstream: &kong.Upstream{ + Slots: kong.Int(defaultSlots), + Healthchecks: &kong.Healthcheck{ + Active: &kong.ActiveHealthcheck{ + Concurrency: kong.Int(defaultConcurrency), + 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("/"), + }, + Target: &kong.Target{ + Weight: kong.Int(defaultWeight), + }, +} + +var defaulterTestOpts = utils.DefaulterOpts{ + KongDefaults: kongDefaults, + DisableDynamicDefaults: false, +} + func emptyState() *state.KongState { s, _ := state.NewKongState() return s @@ -309,7 +384,9 @@ func Test_stateBuilder_services(t *testing.T) { name: "matches ID of an existing service", fields: fields{ targetContent: &Content{ - Info: &Info{}, + Info: &Info{ + Defaults: kongDefaults, + }, Services: []FService{ { Service: kong.Service{ @@ -339,6 +416,9 @@ func Test_stateBuilder_services(t *testing.T) { name: "process a non-existent service", fields: fields{ targetContent: &Content{ + Info: &Info{ + Defaults: kongDefaults, + }, Services: []FService{ { Service: kong.Service{ @@ -449,11 +529,12 @@ func Test_stateBuilder_ingestRoute(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ currentState: tt.fields.currentState, } b.rawState = &utils.KongRawState{} - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.intermediate, _ = state.NewKongState() if err := b.ingestRoute(tt.args.route); (err != nil) != tt.wantErr { @@ -555,11 +636,12 @@ func Test_stateBuilder_ingestTargets(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ currentState: tt.fields.currentState, } b.rawState = &utils.KongRawState{} - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d if err := b.ingestTargets(tt.args.targets); (err != nil) != tt.wantErr { t.Errorf("stateBuilder.ingestPlugins() error = %v, wantErr %v", err, tt.wantErr) @@ -1188,6 +1270,7 @@ func Test_stateBuilder_consumers(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, @@ -1196,7 +1279,7 @@ func Test_stateBuilder_consumers(t *testing.T) { if tt.fields.kongVersion != nil { b.kongVersion = *tt.fields.kongVersion } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) @@ -1359,11 +1442,12 @@ func Test_stateBuilder_certificates(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) @@ -1432,11 +1516,12 @@ func Test_stateBuilder_caCertificates(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) @@ -1460,7 +1545,9 @@ func Test_stateBuilder_upstream(t *testing.T) { name: "process a non-existent upstream", fields: fields{ targetContent: &Content{ - Info: &Info{}, + Info: &Info{ + Defaults: kongDefaults, + }, Upstreams: []FUpstream{ { Upstream: kong.Upstream{ @@ -1525,6 +1612,9 @@ func Test_stateBuilder_upstream(t *testing.T) { name: "matches ID of an existing service", fields: fields{ targetContent: &Content{ + Info: &Info{ + Defaults: kongDefaults, + }, Upstreams: []FUpstream{ { Upstream: kong.Upstream{ @@ -1588,6 +1678,9 @@ func Test_stateBuilder_upstream(t *testing.T) { name: "multiple upstreams are handled correctly", fields: fields{ targetContent: &Content{ + Info: &Info{ + Defaults: kongDefaults, + }, Upstreams: []FUpstream{ { Upstream: kong.Upstream{ @@ -1699,11 +1792,12 @@ func Test_stateBuilder_upstream(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) @@ -1802,11 +1896,12 @@ func Test_stateBuilder_documents(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.konnectRawState) @@ -1829,7 +1924,9 @@ func Test_stateBuilder(t *testing.T) { name: "end to end test with all entities", fields: fields{ targetContent: &Content{ - Info: &Info{}, + Info: &Info{ + Defaults: kongDefaults, + }, Services: []FService{ { Service: kong.Service{ @@ -2346,12 +2443,13 @@ func Test_stateBuilder(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() rand.Seed(42) b := &stateBuilder{ targetContent: tt.fields.targetContent, currentState: tt.fields.currentState, } - d, _ := utils.GetKongDefaulter() + d, _ := utils.GetDefaulter(ctx, defaulterTestOpts) b.defaulter = d b.build() assert.Equal(tt.want, b.rawState) @@ -2439,3 +2537,71 @@ func Test_stateBuilder_fillPluginConfig(t *testing.T) { }) } } + +func Test_getStripPathBasedOnProtocols(t *testing.T) { + tests := []struct { + name string + route kong.Route + wantErr bool + expectedStripPath *bool + }{ + { + name: "true strip_path and grpc protocols", + route: kong.Route{ + Protocols: []*string{kong.String("grpc")}, + StripPath: kong.Bool(true), + }, + wantErr: true, + }, + { + name: "true strip_path and grpcs protocol", + route: kong.Route{ + Protocols: []*string{kong.String("grpcs")}, + StripPath: kong.Bool(true), + }, + wantErr: true, + }, + { + name: "no strip_path and http protocol", + route: kong.Route{ + Protocols: []*string{kong.String("http")}, + }, + expectedStripPath: nil, + }, + { + name: "no strip_path and grpc protocol", + route: kong.Route{ + Protocols: []*string{kong.String("grpc")}, + }, + expectedStripPath: kong.Bool(false), + }, + { + name: "no strip_path and grpcs protocol", + route: kong.Route{ + Protocols: []*string{kong.String("grpcs")}, + }, + expectedStripPath: kong.Bool(false), + }, + { + name: "false strip_path and grpc protocol", + route: kong.Route{ + Protocols: []*string{kong.String("grpc")}, + StripPath: kong.Bool(false), + }, + expectedStripPath: kong.Bool(false), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stripPath, err := getStripPathBasedOnProtocols(tt.route) + if (err != nil) != tt.wantErr { + t.Errorf("getStripPathBasedOnProtocols() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && tt.expectedStripPath != nil { + assert.Equal(t, *tt.expectedStripPath, *stripPath) + } else { + assert.Equal(t, tt.expectedStripPath, stripPath) + } + }) + } +} diff --git a/file/kong_json_schema.json b/file/kong_json_schema.json index 877f36d35..9a0e18335 100644 --- a/file/kong_json_schema.json +++ b/file/kong_json_schema.json @@ -183,6 +183,9 @@ "cert": { "type": "string" }, + "cert_digest": { + "type": "string" + }, "created_at": { "type": "integer" }, @@ -216,6 +219,9 @@ "cert": { "type": "string" }, + "cert_alt": { + "type": "string" + }, "created_at": { "type": "integer" }, @@ -225,6 +231,9 @@ "key": { "type": "string" }, + "key_alt": { + "type": "string" + }, "snis": { "items": { "type": "string" @@ -273,6 +282,9 @@ "cert": { "type": "string" }, + "cert_digest": { + "type": "string" + }, "created_at": { "type": "integer" }, @@ -690,6 +702,9 @@ "created_at": { "type": "integer" }, + "enabled": { + "type": "boolean" + }, "host": { "type": "string" }, @@ -1364,6 +1379,9 @@ "created_at": { "type": "integer" }, + "enabled": { + "type": "boolean" + }, "host": { "type": "string" }, diff --git a/file/reader.go b/file/reader.go index 9fe4380c0..842e6d13f 100644 --- a/file/reader.go +++ b/file/reader.go @@ -52,6 +52,7 @@ func GetForKonnect(ctx context.Context, fileContent *Content, builder.kongVersion = opt.KongVersion builder.client = client builder.ctx = ctx + builder.disableDynamicDefaults = true if fileContent.Transform != nil && !*fileContent.Transform { return nil, nil, ErrorTransformFalseNotSupported diff --git a/file/reader_test.go b/file/reader_test.go index 5fadfde7f..acf1e7bb9 100644 --- a/file/reader_test.go +++ b/file/reader_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/kong/deck/dump" - "github.com/kong/deck/utils" "github.com/kong/go-kong/kong" "github.com/stretchr/testify/assert" ) @@ -103,18 +102,12 @@ func TestTransformNotFalse(t *testing.T) { panic(err) } - config := utils.KongClientConfig{Address: "http://localhost:8001"} - wsClient, err := utils.GetKongClient(config) - if err != nil { - panic(err) - } - ctx := context.Background() - parsed, err := Get(ctx, c, RenderConfig{}, dump.Config{}, wsClient) + parsed, err := Get(ctx, c, RenderConfig{}, dump.Config{}, nil) assert.Equal(err, ErrorTransformFalseNotSupported) assert.Nil(parsed) - parsed, _, err = GetForKonnect(ctx, c, RenderConfig{}, wsClient) + parsed, _, err = GetForKonnect(ctx, c, RenderConfig{}, nil) assert.Equal(err, ErrorTransformFalseNotSupported) assert.Nil(parsed) } diff --git a/go.mod b/go.mod index 5428e5ced..b7dda2c33 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/go-memdb v1.3.2 github.com/hexops/gotextdiff v1.0.3 github.com/imdario/mergo v0.3.12 - github.com/kong/go-kong v0.25.2-0.20220123181121-e965de22d701 + github.com/kong/go-kong v0.27.0 github.com/mitchellh/go-homedir v1.1.0 github.com/sergi/go-diff v1.2.0 // indirect github.com/shirou/gopsutil/v3 v3.21.12 @@ -31,7 +31,7 @@ require ( github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - k8s.io/code-generator v0.23.1 + k8s.io/code-generator v0.23.3 ) require ( @@ -68,7 +68,7 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/tidwall/gjson v1.13.0 // indirect + github.com/tidwall/gjson v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.9 // indirect diff --git a/go.sum b/go.sum index 5a8e53e24..1591baf71 100644 --- a/go.sum +++ b/go.sum @@ -142,7 +142,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -231,7 +230,6 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -301,8 +299,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kong/go-kong v0.25.2-0.20220123181121-e965de22d701 h1:XQrLP/S9GG01Fe9xfrF4NKQ9hOIe127bM0nO2rv8hus= -github.com/kong/go-kong v0.25.2-0.20220123181121-e965de22d701/go.mod h1:B5x5xf7jfGdRlK5Zty3DvUCMpT6VAtxEnm00ZgYcpsg= +github.com/kong/go-kong v0.27.0 h1:u4b/NsuiOqEIc4v/hxmK5H2X1p7RyVv6ofyNLvoW+8s= +github.com/kong/go-kong v0.27.0/go.mod h1:bbC56mBqvnYZmzAWujmUqONXhloPWLs8r4A07uT8EzE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -418,7 +416,6 @@ github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= @@ -439,8 +436,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -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/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= +github.com/tidwall/gjson v1.14.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= @@ -576,7 +573,6 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= @@ -942,19 +938,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/code-generator v0.22.4/go.mod h1:qjYl54pQ/emhkT0UxbufbREYJMWsHNNV/jSVwhYZQGw= -k8s.io/code-generator v0.23.1 h1:ViFOlP/0bYD7VrnUDS+ch5ej5EIuMawFmHcRuv9Yxyw= -k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/code-generator v0.23.3 h1:NSAKIkvkL8OaWr5DrF9CXGBJjhMp3itplT/6fwHQcAY= +k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c h1:GohjlNKauSai7gN4wsJkeZ3WAJx4Sh+oT/b5IYn5suA= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -962,6 +953,6 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/tests/integration/sync_test.go b/tests/integration/sync_test.go index 624919707..d50dbed48 100644 --- a/tests/integration/sync_test.go +++ b/tests/integration/sync_test.go @@ -52,7 +52,7 @@ func testKongState( t.Errorf(err.Error()) } opt := []cmp.Option{ - cmpopts.IgnoreFields(kong.Service{}, "ID", "CreatedAt", "UpdatedAt"), + cmpopts.IgnoreFields(kong.Service{}, "ID", "CreatedAt", "UpdatedAt", "Enabled"), } if diff := cmp.Diff(services, expectedServices, opt...); diff != "" { t.Errorf(diff) diff --git a/utils/defaulter.go b/utils/defaulter.go index 8ee8cd023..2ae852b26 100644 --- a/utils/defaulter.go +++ b/utils/defaulter.go @@ -1,39 +1,72 @@ package utils import ( + "context" "fmt" "reflect" "github.com/imdario/mergo" + "github.com/kong/go-kong/kong" ) // Defaulter registers types and fills in struct fields with // default values. type Defaulter struct { r map[string]interface{} + + ctx context.Context + client *kong.Client + + service *kong.Service + route *kong.Route + upstream *kong.Upstream + target *kong.Target } -// GetKongDefaulter returns a defaulter which can set default values -// for Kong entities. -func GetKongDefaulter() (*Defaulter, error) { - var d Defaulter - err := d.Register(&serviceDefaults) +type DefaulterOpts struct { + KongDefaults interface{} + DisableDynamicDefaults bool + Client *kong.Client +} + +// NewDefaulter initializes a Defaulter with empty entities. +func NewDefaulter() *Defaulter { + return &Defaulter{ + service: &kong.Service{}, + route: &kong.Route{}, + upstream: &kong.Upstream{}, + target: &kong.Target{}, + } +} + +func getKongDefaulter(opts DefaulterOpts) (*Defaulter, error) { + d := NewDefaulter() + if err := d.populateDefaultsFromInput(opts.KongDefaults); err != nil { + return nil, err + } + if opts.DisableDynamicDefaults { + if err := d.populateStaticDefaultsForKonnect(); err != nil { + return nil, err + } + } + + err := d.Register(d.service) if err != nil { return nil, fmt.Errorf("registering service with defaulter: %w", err) } - err = d.Register(&routeDefaults) + err = d.Register(d.route) if err != nil { return nil, fmt.Errorf("registering route with defaulter: %w", err) } - err = d.Register(&upstreamDefaults) + err = d.Register(d.upstream) if err != nil { return nil, fmt.Errorf("registering upstream with defaulter: %w", err) } - err = d.Register(&targetDefaults) + err = d.Register(d.target) if err != nil { return nil, fmt.Errorf("registering target with defaulter: %w", err) } - return &d, nil + return d, nil } func (d *Defaulter) once() { @@ -126,3 +159,141 @@ func (d *Defaulter) MustSet(arg interface{}) { panic(err) } } + +func (d *Defaulter) getEntitySchema(entityType string) (map[string]interface{}, error) { + var schema map[string]interface{} + schema, err := d.client.Schemas.Get(d.ctx, entityType) + if err != nil { + return schema, err + } + return schema, nil +} + +func (d *Defaulter) addEntityDefaults(entityType string, entity interface{}) error { + schema, err := d.getEntitySchema(entityType) + if err != nil { + return fmt.Errorf("retrieve schema for %v from Kong: %v", entityType, err) + } + return kong.FillEntityDefaults(entity, schema) +} + +func getKongDefaulterWithClient(ctx context.Context, opts DefaulterOpts) (*Defaulter, error) { + // fills defaults from input + d, err := getKongDefaulter(opts) + if err != nil { + return nil, err + } + d.ctx = ctx + d.client = opts.Client + + // fills defaults from Kong API + if err := d.addEntityDefaults("services", d.service); err != nil { + return nil, fmt.Errorf("get defaults for services: %v", err) + } + if err := d.Register(d.service); err != nil { + return nil, fmt.Errorf("registering service with defaulter: %w", err) + } + + if err := d.addEntityDefaults("routes", d.route); err != nil { + return nil, fmt.Errorf("get defaults for routes: %v", err) + } + if err := d.Register(d.route); err != nil { + return nil, fmt.Errorf("registering route with defaulter: %w", err) + } + + if err := d.addEntityDefaults("upstreams", d.upstream); err != nil { + return nil, fmt.Errorf("get defaults for upstreams: %v", err) + } + if err := d.Register(d.upstream); err != nil { + return nil, fmt.Errorf("registering upstream with defaulter: %w", err) + } + + if err := d.addEntityDefaults("targets", d.target); err != nil { + return nil, fmt.Errorf("get defaults for targets: %v", err) + } + if err := d.Register(d.target); err != nil { + return nil, fmt.Errorf("registering target with defaulter: %w", err) + } + return d, nil +} + +// GetDefaulter returns a Defaulter object to be used to set defaults +// on Kong entities. The order of precedence is as follow, from higher to lower: +// +// 1. values set in the state file +// 2. values set in the {_info: defaults:} object in the state file +// 3. schema defaults coming from Admin API (excluded Konnect) +// 4. hardcoded defaults under utils/constants.go (Konnect-only) +func GetDefaulter(ctx context.Context, opts DefaulterOpts) (*Defaulter, error) { + if opts.Client != nil && !opts.DisableDynamicDefaults { + return getKongDefaulterWithClient(ctx, opts) + } + opts.DisableDynamicDefaults = true + return getKongDefaulter(opts) +} + +func (d *Defaulter) populateDefaultsFromInput(defaults interface{}) error { + r := reflect.ValueOf(defaults) + + service := reflect.Indirect(r).FieldByName("Service") + serviceObj := service.Interface().(*kong.Service) + if serviceObj != nil { + err := mergo.Merge(d.service, serviceObj, mergo.WithTransformers(kongTransformer{})) + if err != nil { + return fmt.Errorf("merging: %w", err) + } + } + + route := reflect.Indirect(r).FieldByName("Route") + routeObj := route.Interface().(*kong.Route) + if routeObj != nil { + err := mergo.Merge(d.route, routeObj, mergo.WithTransformers(kongTransformer{})) + if err != nil { + return fmt.Errorf("merging: %w", err) + } + } + + upstream := reflect.Indirect(r).FieldByName("Upstream") + upstreamObj := upstream.Interface().(*kong.Upstream) + if upstreamObj != nil { + err := mergo.Merge(d.upstream, upstreamObj, mergo.WithTransformers(kongTransformer{})) + if err != nil { + return fmt.Errorf("merging: %w", err) + } + } + + target := reflect.Indirect(r).FieldByName("Target") + targetObj := target.Interface().(*kong.Target) + if targetObj != nil { + err := mergo.Merge(d.target, targetObj, mergo.WithTransformers(kongTransformer{})) + if err != nil { + return fmt.Errorf("merging: %w", err) + } + } + return nil +} + +func (d *Defaulter) populateStaticDefaultsForKonnect() error { + if err := mergo.Merge( + d.service, &serviceDefaults, mergo.WithTransformers(kongTransformer{}), + ); err != nil { + return fmt.Errorf("merging service static defaults: %w", err) + } + if err := mergo.Merge( + d.route, &routeDefaults, mergo.WithTransformers(kongTransformer{}), + ); err != nil { + return fmt.Errorf("merging route static defaults: %w", err) + } + if err := mergo.Merge( + d.upstream, &upstreamDefaults, mergo.WithTransformers(kongTransformer{}), + ); err != nil { + return fmt.Errorf("merging upstream static defaults: %w", err) + } + if err := mergo.Merge( + d.target, &targetDefaults, mergo.WithTransformers(kongTransformer{}), + ); err != nil { + return fmt.Errorf("merging target static defaults: %w", err) + } + + return nil +} diff --git a/utils/defaulter_test.go b/utils/defaulter_test.go index 7eea6c4d6..4b909f5ff 100644 --- a/utils/defaulter_test.go +++ b/utils/defaulter_test.go @@ -1,12 +1,33 @@ package utils import ( + "context" + "reflect" "testing" "github.com/kong/go-kong/kong" "github.com/stretchr/testify/assert" ) +type kongDefaultForTesting struct { + Service *kong.Service + Route *kong.Route + Upstream *kong.Upstream + Target *kong.Target +} + +var kongDefaults = kongDefaultForTesting{ + Service: &serviceDefaults, + Route: &routeDefaults, + Upstream: &upstreamDefaults, + Target: &targetDefaults, +} + +var defaulterTestOpts = DefaulterOpts{ + KongDefaults: kongDefaults, + DisableDynamicDefaults: false, +} + func TestDefaulter(t *testing.T) { assert := assert.New(t) @@ -54,7 +75,8 @@ func TestDefaulter(t *testing.T) { func TestServiceSetTest(t *testing.T) { assert := assert.New(t) - d, err := GetKongDefaulter() + ctx := context.Background() + d, err := GetDefaulter(ctx, defaulterTestOpts) assert.NotNil(d) assert.Nil(err) @@ -139,7 +161,8 @@ func TestServiceSetTest(t *testing.T) { func TestRouteSetTest(t *testing.T) { assert := assert.New(t) - d, err := GetKongDefaulter() + ctx := context.Background() + d, err := GetDefaulter(ctx, defaulterTestOpts) assert.NotNil(d) assert.Nil(err) @@ -231,7 +254,8 @@ func TestRouteSetTest(t *testing.T) { func TestUpstreamSetTest(t *testing.T) { assert := assert.New(t) - d, err := GetKongDefaulter() + ctx := context.Background() + d, err := GetDefaulter(ctx, defaulterTestOpts) assert.NotNil(d) assert.Nil(err) @@ -480,3 +504,71 @@ func TestUpstreamSetTest(t *testing.T) { }) } } + +func TestGetDefaulter_Konnect(t *testing.T) { + assert := assert.New(t) + + testCases := []struct { + desc string + opts DefaulterOpts + want *Defaulter + }{ + { + desc: "empty user defaults", + opts: DefaulterOpts{ + KongDefaults: &kongDefaultForTesting{}, + DisableDynamicDefaults: true, + }, + want: &Defaulter{ + service: &serviceDefaults, + route: &routeDefaults, + upstream: &upstreamDefaults, + target: &targetDefaults, + }, + }, + { + desc: "user defaults take precedence", + opts: DefaulterOpts{ + KongDefaults: &kongDefaultForTesting{ + Service: &kong.Service{ + Port: kong.Int(8080), + }, + }, + DisableDynamicDefaults: true, + }, + want: &Defaulter{ + service: &kong.Service{ + Port: kong.Int(8080), + Protocol: kong.String("http"), + ConnectTimeout: kong.Int(defaultTimeout), + WriteTimeout: kong.Int(defaultTimeout), + ReadTimeout: kong.Int(defaultTimeout), + }, + route: &routeDefaults, + upstream: &upstreamDefaults, + target: &targetDefaults, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + ctx := context.Background() + d, err := GetDefaulter(ctx, tc.opts) + assert.NotNil(d) + assert.Nil(err) + + if !reflect.DeepEqual(d.service, tc.want.service) { + assert.Equal(t, tc.want.service, d.service) + } + if !reflect.DeepEqual(d.route, tc.want.route) { + assert.Equal(t, tc.want.route, d.route) + } + if !reflect.DeepEqual(d.upstream, tc.want.upstream) { + assert.Equal(t, tc.want.upstream, d.upstream) + } + if !reflect.DeepEqual(d.target, tc.want.target) { + assert.Equal(t, tc.want.target, d.target) + } + }) + } +}