diff --git a/cmd/gateway_validate.go b/cmd/gateway_validate.go index 762032f43..557c7e27e 100644 --- a/cmd/gateway_validate.go +++ b/cmd/gateway_validate.go @@ -23,6 +23,7 @@ var ( validateWorkspace string validateParallelism int validateJSONOutput bool + validateKonnectCompatibility bool ) func executeValidate(cmd *cobra.Command, _ []string) error { @@ -104,6 +105,12 @@ func executeValidate(cmd *cobra.Command, _ []string) error { return err } + if validateKonnectCompatibility { + if errs := validate.KonnectCompatibility(targetContent); len(errs) != 0 { + return validate.ErrorsWrapper{Errors: errs} + } + } + if validateOnline { if errs := validateWithKong(ctx, kongClient, ks, targetContent.FormatVersion); len(errs) != 0 { return validate.ErrorsWrapper{Errors: errs} @@ -209,6 +216,8 @@ this command unless --online flag is used. 10, "Maximum number of concurrent requests to Kong.") validateCmd.Flags().BoolVar(&validateJSONOutput, "json-output", false, "generate command execution report in a JSON format") + validateCmd.Flags().BoolVar(&validateKonnectCompatibility, "konnect-compatibility", + false, "validate that the state file(s) are ready to be deployed to Konnect") if err := ensureGetAllMethods(); err != nil { panic(err.Error()) diff --git a/validate/konnect_compatibility.go b/validate/konnect_compatibility.go new file mode 100644 index 000000000..74eb617b7 --- /dev/null +++ b/validate/konnect_compatibility.go @@ -0,0 +1,127 @@ +package validate + +import ( + "errors" + "fmt" + "strconv" + + "github.com/kong/go-database-reconciler/pkg/file" + "github.com/kong/go-kong/kong" +) + +var ( + errKonnect = "[konnect] section not specified - ensure details are set via cli flags" + errWorkspace = "[workspaces] not supported by Konnect - use control planes instead" + errNoVersion = "[version] unable to determine decK file version" + errBadVersion = fmt.Sprintf("[version] decK file version must be '%.1f' or greater", supportedVersion) + errPluginIncompatible = "[%s] plugin is not compatible with Konnect" + errPluginNoCluster = "[%s] plugin can't be used with cluster strategy" +) + +var supportedVersion = 3.0 + +func checkPlugin(name *string, config kong.Configuration) error { + switch *name { + case "jwt-signer", "vault-auth", "oauth2": + return fmt.Errorf(errPluginIncompatible, *name) + case "application-registration": + return fmt.Errorf("[%s] available in Konnect, but doesn't require this plugin", *name) + case "key-auth-enc": + return fmt.Errorf("[%s] keys are automatically encrypted in Konnect, use the key auth plugin instead", *name) + case "openwhisk": + return fmt.Errorf("[%s] plugin not bundled with Kong Gateway - installed as a LuaRocks package", *name) + case "rate-limiting", "rate-limiting-advanced", "response-ratelimiting", "graphql-rate-limiting-advanced": + if config["strategy"] == "cluster" { + return fmt.Errorf(errPluginNoCluster, *name) + } + default: + } + return nil +} + +func KonnectCompatibility(targetContent *file.Content) []error { + var errs []error + + if targetContent.Workspace != "" { + errs = append(errs, errors.New(errWorkspace)) + } + + if targetContent.Konnect == nil { + errs = append(errs, errors.New(errKonnect)) + } + + versionNumber, err := strconv.ParseFloat(targetContent.FormatVersion, 32) + if err != nil { + errs = append(errs, errors.New(errNoVersion)) + } else { + if versionNumber < supportedVersion { + errs = append(errs, errors.New(errBadVersion)) + } + } + + for _, plugin := range targetContent.Plugins { + if plugin.Enabled != nil && *plugin.Enabled && plugin.Config != nil { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + + for _, consumer := range targetContent.Consumers { + for _, plugin := range consumer.Plugins { + if plugin.Enabled != nil && *plugin.Enabled && plugin.Config != nil { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + } + + for _, consumerGroup := range targetContent.ConsumerGroups { + for _, plugin := range consumerGroup.Plugins { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + + for _, service := range targetContent.Services { + for _, plugin := range service.Plugins { + if plugin.Enabled != nil && *plugin.Enabled && plugin.Config != nil { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + } + + for _, service := range targetContent.Services { + for _, route := range service.Routes { + for _, plugin := range route.Plugins { + if plugin.Enabled != nil && *plugin.Enabled && plugin.Config != nil { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + } + } + + for _, route := range targetContent.Routes { + for _, plugin := range route.Plugins { + if plugin.Enabled != nil && *plugin.Enabled && plugin.Config != nil { + err := checkPlugin(plugin.Name, plugin.Config) + if err != nil { + errs = append(errs, err) + } + } + } + } + + return errs +} diff --git a/validate/konnect_compatibility_test.go b/validate/konnect_compatibility_test.go new file mode 100644 index 000000000..d4a120580 --- /dev/null +++ b/validate/konnect_compatibility_test.go @@ -0,0 +1,143 @@ +package validate + +import ( + "errors" + "fmt" + "testing" + + "github.com/kong/go-database-reconciler/pkg/file" + "github.com/kong/go-kong/kong" + "github.com/stretchr/testify/assert" +) + +func Test_KonnectCompatibility(t *testing.T) { + tests := []struct { + name string + content *file.Content + expected []error + }{ + { + name: "version invalid", + content: &file.Content{ + FormatVersion: "2.9", + Workspace: "test", + Konnect: &file.Konnect{ + RuntimeGroupName: "s", + ControlPlaneName: "s", + }, + }, + expected: []error{ + errors.New(errWorkspace), + errors.New(errBadVersion), + }, + }, + { + name: "no konnect", + content: &file.Content{ + FormatVersion: "3.1", + }, + expected: []error{ + errors.New(errKonnect), + }, + }, + { + name: "incompatible service plugin", + content: &file.Content{ + FormatVersion: "3.1", + Konnect: &file.Konnect{ + RuntimeGroupName: "s", + ControlPlaneName: "s", + }, + Services: []file.FService{ + {Plugins: []*file.FPlugin{ + { + Plugin: kong.Plugin{ + Name: kong.String("oauth2"), + Enabled: kong.Bool(true), + Config: kong.Configuration{"config": "config"}, + }, + }, + }}, + }, + }, + expected: []error{ + fmt.Errorf(errPluginIncompatible, "oauth2"), + }, + }, + { + name: "incompatible service route plugins", + content: &file.Content{ + FormatVersion: "3.1", + Konnect: &file.Konnect{ + RuntimeGroupName: "s", + ControlPlaneName: "s", + }, + Services: []file.FService{ + {Routes: []*file.FRoute{ + { + Plugins: []*file.FPlugin{ + { + Plugin: kong.Plugin{ + Name: kong.String("oauth2"), + Enabled: kong.Bool(true), + Config: kong.Configuration{"config": "config"}, + }, + }, + { + Plugin: kong.Plugin{ + Name: kong.String("key-auth-enc"), + Enabled: kong.Bool(true), + Config: kong.Configuration{"config": "config"}, + }, + }, + }, + }, + }}, + }, + }, + expected: []error{ + fmt.Errorf(errPluginIncompatible, "oauth2"), + fmt.Errorf("[%s] keys are automatically encrypted in Konnect, use the key auth plugin instead", "key-auth-enc"), + }, + }, + { + name: "incompatible top-level and consumer-group plugins", + content: &file.Content{ + FormatVersion: "3.1", + Konnect: &file.Konnect{ + RuntimeGroupName: "s", + ControlPlaneName: "s", + }, + Plugins: []file.FPlugin{ + { + Plugin: kong.Plugin{ + Name: kong.String("response-ratelimiting"), + Enabled: kong.Bool(true), + Config: kong.Configuration{"strategy": "cluster"}, + }, + }, + }, + ConsumerGroups: []file.FConsumerGroupObject{ + { + Plugins: []*kong.ConsumerGroupPlugin{ + { + Name: kong.String("key-auth-enc"), + Config: kong.Configuration{"config": "config"}, + }, + }, + }, + }, + }, + expected: []error{ + fmt.Errorf(errPluginNoCluster, "response-ratelimiting"), + fmt.Errorf("[%s] keys are automatically encrypted in Konnect, use the key auth plugin instead", "key-auth-enc"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := KonnectCompatibility(tt.content) + assert.Equal(t, tt.expected, errs) + }) + } +}