diff --git a/.ci/setup_kong.sh b/.ci/setup_kong.sh index c8aecf0e3..c36193464 100755 --- a/.ci/setup_kong.sh +++ b/.ci/setup_kong.sh @@ -4,9 +4,11 @@ set -e source $(dirname "$0")/_common.sh -KONG_IMAGE=${KONG_IMAGE_REPO:-kong}:${KONG_IMAGE_TAG:-3.3} +KONG_IMAGE=${KONG_IMAGE_REPO:-kong}:${KONG_IMAGE_TAG:-3.4} NETWORK_NAME=kong-test +KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR:-'traditional_compatible'} + PG_CONTAINER_NAME=pg DATABASE_USER=kong DATABASE_NAME=kong @@ -32,6 +34,7 @@ function deploy_kong_postgres() -e "KONG_ADMIN_GUI_AUTH=basic-auth" \ -e "KONG_ENFORCE_RBAC=on" \ -e "KONG_PORTAL=on" \ + -e "KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR}" \ -p 8000:8000 \ -p 8443:8443 \ -p 127.0.0.1:8001:8001 \ @@ -53,6 +56,7 @@ function deploy_kong_dbless() -e "KONG_ADMIN_GUI_AUTH=basic-auth" \ -e "KONG_ENFORCE_RBAC=on" \ -e "KONG_PORTAL=on" \ + -e "KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR}" \ -p 8000:8000 \ -p 8443:8443 \ -p 127.0.0.1:8001:8001 \ diff --git a/.ci/setup_kong_ee.sh b/.ci/setup_kong_ee.sh index d6f576170..079c77973 100755 --- a/.ci/setup_kong_ee.sh +++ b/.ci/setup_kong_ee.sh @@ -4,9 +4,11 @@ set -e source $(dirname "$0")/_common.sh -KONG_IMAGE=${KONG_IMAGE_REPO:-kong/kong-gateway}:${KONG_IMAGE_TAG:-3.3} +KONG_IMAGE=${KONG_IMAGE_REPO:-kong/kong-gateway}:${KONG_IMAGE_TAG:-3.4} NETWORK_NAME=kong-test +KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR:-'traditional_compatible'} + PG_CONTAINER_NAME=pg DATABASE_USER=kong DATABASE_NAME=kong @@ -44,6 +46,7 @@ function deploy_kong_ee() -e "KONG_ENFORCE_RBAC=on" \ -e "KONG_PORTAL=on" \ -e "KONG_ADMIN_GUI_SESSION_CONF={}" \ + -e "KONG_ROUTER_FLAVOR=${KONG_ROUTER_FLAVOR}" \ -p 8000:8000 \ -p 8443:8443 \ -p 8001:8001 \ diff --git a/.github/workflows/integration-test-enterprise-nightly.yaml b/.github/workflows/integration-test-enterprise-nightly.yaml index fcc25f3db..2188a70b9 100644 --- a/.github/workflows/integration-test-enterprise-nightly.yaml +++ b/.github/workflows/integration-test-enterprise-nightly.yaml @@ -31,11 +31,17 @@ jobs: fi test-enterprise: + strategy: + matrix: + router_flavor: + - 'traditional_compatible' + - 'expressions' continue-on-error: true needs: - secret-available if: needs.secret-available.outputs.ok env: + KONG_ROUTER_FLAVOR: ${{ matrix.router_flavor }} KONG_ADMIN_TOKEN: kong KONG_IMAGE_REPO: "kong/kong-gateway-internal" KONG_IMAGE_TAG: "master-alpine" diff --git a/.github/workflows/integration-test-enterprise.yaml b/.github/workflows/integration-test-enterprise.yaml index 6b00809f8..2fbcd3fa0 100644 --- a/.github/workflows/integration-test-enterprise.yaml +++ b/.github/workflows/integration-test-enterprise.yaml @@ -37,6 +37,27 @@ jobs: if: needs.secret-available.outputs.ok strategy: matrix: + # Skip explicitly to avoid spawning many unnecessary jobs, + # since expressions router is supported for Kong >= 3.0. + # Option router_flavor is ignored for Kong < 3.0. + exclude: + - kong_version: '2.2' + router_flavor: 'expressions' + - kong_version: '2.3' + router_flavor: 'expressions' + - kong_version: '2.4' + router_flavor: 'expressions' + - kong_version: '2.5' + router_flavor: 'expressions' + - kong_version: '2.6' + router_flavor: 'expressions' + - kong_version: '2.7' + router_flavor: 'expressions' + - kong_version: '2.8' + router_flavor: 'expressions' + router_flavor: + - 'traditional_compatible' + - 'expressions' kong_version: - '2.2' - '2.3' @@ -49,7 +70,9 @@ jobs: - '3.1' - '3.2' - '3.3' + - '3.4' env: + KONG_ROUTER_FLAVOR: ${{ matrix.router_flavor }} KONG_IMAGE_TAG: ${{ matrix.kong_version }} KONG_ANONYMOUS_REPORTS: "off" KONG_ADMIN_TOKEN: kong diff --git a/.github/workflows/integration-test-nightly.yaml b/.github/workflows/integration-test-nightly.yaml index 805e9f579..92ec9c77c 100644 --- a/.github/workflows/integration-test-nightly.yaml +++ b/.github/workflows/integration-test-nightly.yaml @@ -22,7 +22,11 @@ jobs: dbmode: - 'dbless' - 'postgres' + router_flavor: + - 'traditional_compatible' + - 'expressions' env: + KONG_ROUTER_FLAVOR: ${{ matrix.router_flavor }} KONG_IMAGE_REPO: "kong/kong" KONG_IMAGE_TAG: "master-alpine" KONG_ANONYMOUS_REPORTS: "off" @@ -45,4 +49,3 @@ jobs: name: codecov-nightly flags: nightly,integration,community fail_ci_if_error: true - diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index fa8fbe8e7..ad1548133 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -19,23 +19,50 @@ jobs: test: strategy: matrix: - kong_version: - - '2.1' - - '2.2' - - '2.3' - - '2.4' - - '2.5' - - '2.6' - - '2.7' - - '2.8' - - '3.0' - - '3.1' - - '3.2' - - '3.3' + # Skip explicitly to avoid spawning many unnecessary jobs, + # since expressions router is supported for Kong >= 3.0. + # Option router_flavor is ignored for Kong < 3.0. + exclude: + - kong_version: '2.1' + router_flavor: 'expressions' + - kong_version: '2.2' + router_flavor: 'expressions' + - kong_version: '2.2' + router_flavor: 'expressions' + - kong_version: '2.3' + router_flavor: 'expressions' + - kong_version: '2.4' + router_flavor: 'expressions' + - kong_version: '2.5' + router_flavor: 'expressions' + - kong_version: '2.6' + router_flavor: 'expressions' + - kong_version: '2.7' + router_flavor: 'expressions' + - kong_version: '2.8' + router_flavor: 'expressions' dbmode: - 'dbless' - 'postgres' + router_flavor: + - 'traditional_compatible' + - 'expressions' + kong_version: + - '2.1' + - '2.2' + - '2.3' + - '2.4' + - '2.5' + - '2.6' + - '2.7' + - '2.8' + - '3.0' + - '3.1' + - '3.2' + - '3.3' + - '3.4' env: + KONG_ROUTER_FLAVOR: ${{ matrix.router_flavor }} KONG_IMAGE_TAG: ${{ matrix.kong_version }} KONG_ANONYMOUS_REPORTS: "off" runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index f877547ab..287b8db73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Table of Contents +- [v0.47.0](#v0470) - [v0.46.0](#v0460) - [v0.45.0](#v0450) - [v0.44.0](#v0440) @@ -60,6 +61,13 @@ ## Unreleased +## [v0.47.0] + +> Release date: 2023/08/29 + +- Added method `Validate` to `RouteService` + [#368](https://github.com/Kong/go-kong/pull/368) + ## [v0.46.0] > Release date: 2023/07/17 diff --git a/kong/client_test.go b/kong/client_test.go index a1d903e56..ea64f6078 100644 --- a/kong/client_test.go +++ b/kong/client_test.go @@ -227,7 +227,7 @@ func TestBaseRootURL(t *testing.T) { func TestReloadDeclarativeRawConfig(t *testing.T) { RunWhenDBMode(t, "off") - + SkipWhenKongRouterFlavor(t, Expressions) tests := []struct { name string config Configuration diff --git a/kong/plugin_service_test.go b/kong/plugin_service_test.go index 7b1e3643f..9c45c4a39 100644 --- a/kong/plugin_service_test.go +++ b/kong/plugin_service_test.go @@ -42,6 +42,7 @@ func TestPluginsServiceValidation(T *testing.T) { func TestPluginsService(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) @@ -403,6 +404,7 @@ func TestPluginListEndpoint(T *testing.T) { func TestPluginListAllForEntityEndpoint(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) @@ -474,7 +476,7 @@ func TestPluginListAllForEntityEndpoint(T *testing.T) { }, } - // create fixturs + // create fixtures for i := 0; i < len(plugins); i++ { schema, err := client.Plugins.GetFullSchema(defaultCtx, plugins[i].Name) assert.NoError(err) diff --git a/kong/route_service.go b/kong/route_service.go index 354f389b6..21e6940ff 100644 --- a/kong/route_service.go +++ b/kong/route_service.go @@ -3,7 +3,9 @@ package kong import ( "context" "encoding/json" + "errors" "fmt" + "net/http" ) // AbstractRouteService handles routes in Kong. @@ -24,6 +26,8 @@ type AbstractRouteService interface { ListAll(ctx context.Context) ([]*Route, error) // ListForService fetches a list of Routes in Kong associated with a service. ListForService(ctx context.Context, serviceNameOrID *string, opt *ListOpt) ([]*Route, *ListOpt, error) + // Validate validates a Route against its schema (checks validity of provided regex too). + Validate(ctx context.Context, route *Route) (bool, string, error) } // RouteService handles routes in Kong. @@ -208,3 +212,19 @@ func (s *RouteService) ListForService(ctx context.Context, return routes, next, nil } + +// Validate validates a Route against its schema (checks validity of provided regex too). +func (s *RouteService) Validate(ctx context.Context, route *Route) (bool, string, error) { + req, err := s.client.NewRequest(http.MethodPost, "/schemas/routes/validate", nil, &route) + if err != nil { + return false, "", err + } + if _, err := s.client.Do(ctx, req, nil); err != nil { + apiErr := &APIError{} + if ok := errors.As(err, &apiErr); !ok || apiErr.Code() != http.StatusBadRequest { + return false, "", err + } + return false, apiErr.message, nil + } + return true, "", nil +} diff --git a/kong/route_service_test.go b/kong/route_service_test.go index 060062dab..8f5c5be79 100644 --- a/kong/route_service_test.go +++ b/kong/route_service_test.go @@ -1,6 +1,7 @@ package kong import ( + "strings" "testing" "github.com/google/uuid" @@ -10,6 +11,7 @@ import ( func TestRoutesRoute(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) @@ -103,6 +105,8 @@ func TestRoutesRoute(T *testing.T) { func TestRouteWithTags(T *testing.T) { RunWhenDBMode(T, "postgres") RunWhenKong(T, ">=1.1.0") + SkipWhenKongRouterFlavor(T, Expressions) + require := require.New(T) client, err := NewTestClient(nil, nil) @@ -126,6 +130,7 @@ func TestRouteWithTags(T *testing.T) { func TestCreateInRoute(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) @@ -166,6 +171,7 @@ func TestCreateInRoute(T *testing.T) { func TestRouteListEndpoint(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) @@ -278,6 +284,8 @@ func compareRoutes(T *testing.T, expected, actual []*Route) bool { func TestRouteWithHeaders(T *testing.T) { RunWhenDBMode(T, "postgres") RunWhenKong(T, ">=1.3.0") + SkipWhenKongRouterFlavor(T, Expressions) + assert := assert.New(T) require := require.New(T) @@ -302,3 +310,123 @@ func TestRouteWithHeaders(T *testing.T) { err = client.Routes.Delete(defaultCtx, createdRoute.ID) assert.NoError(err) } + +func TestRoutesValidationExpressions(T *testing.T) { + RunWhenKong(T, ">=3.0.0") + SkipWhenKongRouterFlavor(T, Traditional, TraditionalCompatible) + + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + const errMsgStart = "schema violation (Router Expression failed validation:" + for _, tC := range []struct { + name string + route *Route + valid bool + msgStartWith string + }{ + { + name: "invalid expression - nonexisting LHS field", + route: &Route{ + Expression: String("net.foo == 3000"), + }, + msgStartWith: errMsgStart, + }, + { + name: "invalid expression - invalid regex", + route: &Route{ + Expression: String(`lower(http.path) ~ "pref~[[[[[ix"`), + }, + msgStartWith: errMsgStart, + }, + { + name: "valid expression", + route: &Route{ + Expression: String(`lower(http.path) ^= "/prefix/"`), + }, + valid: true, + }, + } { + T.Run(tC.name, func(t *testing.T) { + ok, msg, err := client.Routes.Validate(defaultCtx, tC.route) + require.NoError(err) + require.Equal(tC.valid, ok) + if !ok { + require.NotEmpty(tC.msgStartWith) + require.True(strings.HasPrefix(msg, tC.msgStartWith)) + } + }) + } +} + +func TestRoutesValidationTraditionalCompatible(T *testing.T) { + RunWhenKong(T, ">=3.0.0") + SkipWhenKongRouterFlavor(T, Traditional, Expressions) + + require := require.New(T) + + client, err := NewTestClient(nil, nil) + require.NoError(err) + require.NotNil(client) + + var ( + validPath = String("/prefix/") + validRegex = String("~/payment/(docs|health)$") + invalidRegex = String("~/payment/(docs|health))") + ) + for _, tC := range []struct { + name string + route *Route + valid bool + msgStartWith string + }{ + { + name: "valid path - prefix", + route: &Route{ + Paths: []*string{validPath}, + }, + valid: true, + }, + { + name: "valid path - regex", + route: &Route{ + Paths: []*string{validRegex}, + }, + valid: true, + }, + { + name: "multiple valid paths - prefix and regex", + route: &Route{ + Paths: []*string{validPath, validRegex}, + }, + valid: true, + }, + { + name: "invalid path - invalid regex (unmatched parentheses)", + route: &Route{ + Paths: []*string{invalidRegex}, + }, + msgStartWith: "schema violation (paths.1: invalid regex:", + }, + { + name: "multiple paths - one path with invalid regex", + route: &Route{ + Paths: []*string{validPath, invalidRegex, String("/foo")}, + }, + msgStartWith: "schema violation (paths.2: invalid regex:", + }, + } { + T.Run(tC.name, func(t *testing.T) { + ok, msg, err := client.Routes.Validate(defaultCtx, tC.route) + require.NoError(err) + require.Equal(tC.valid, ok) + if !ok { + require.NotEmpty(tC.msgStartWith) + require.True(strings.HasPrefix(msg, tC.msgStartWith)) + } + }) + } +} diff --git a/kong/service_service_test.go b/kong/service_service_test.go index 8c5a63d3d..a61659b3a 100644 --- a/kong/service_service_test.go +++ b/kong/service_service_test.go @@ -10,6 +10,7 @@ import ( func TestServicesService(T *testing.T) { RunWhenDBMode(T, "postgres") + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) require := require.New(T) diff --git a/kong/test_utils.go b/kong/test_utils.go index 7fdeff15c..091bb5c5b 100644 --- a/kong/test_utils.go +++ b/kong/test_utils.go @@ -1,6 +1,7 @@ package kong import ( + "fmt" "net/http" "os" "testing" @@ -127,6 +128,39 @@ func NewTestClient(baseURL *string, client *http.Client) (*Client, error) { func RunWhenDBMode(t *testing.T, dbmode string) { t.Helper() + dbMode, err := getKongConfigValue(t, "database") + if err != nil { + t.Skip(err.Error()) + } + if dbMode != dbmode { + t.Skipf("detected Kong running in dbmode:%q but requested dbmode:%q", dbMode, dbmode) + } +} + +type RouterFlavor string + +const ( + Traditional RouterFlavor = "traditional" + TraditionalCompatible RouterFlavor = "traditional_compatible" + Expressions RouterFlavor = "expressions" +) + +func SkipWhenKongRouterFlavor(t *testing.T, flavor ...RouterFlavor) { + t.Helper() + + routerFlavor, err := getKongConfigValue(t, "router_flavor") + if err != nil { + t.Skip(err.Error()) + } + for _, f := range flavor { + if RouterFlavor(routerFlavor) == f { + t.Skipf("router flavor:%q skipping", f) + } + } +} + +func getKongConfigValue(t *testing.T, key string) (string, error) { + t.Helper() client, err := NewTestClient(nil, nil) if err != nil { t.Error(err) @@ -138,25 +172,17 @@ func RunWhenDBMode(t *testing.T, dbmode string) { config, ok := info["configuration"] if !ok { - t.Skip("failed to find 'configuration' config key in kong configuration") + return "", fmt.Errorf("failed to find %q config key in kong configuration", key) } configuration, ok := config.(map[string]any) if !ok { - t.Skipf("'configuration' key is not a map but %T", config) + return "", fmt.Errorf("%q key is not a map but %T", key, config) } - dbConfig, ok := configuration["database"] + value, ok := configuration[key].(string) if !ok { - t.Skip("failed to find 'database' config key in kong confiration") - } - - dbMode, ok := dbConfig.(string) - if !ok { - t.Skipf("'database' config key is not a string but %T", dbConfig) - } - - if dbMode != dbmode { - t.Skipf("detected Kong running in dbmode:%q but requested dbmode:%q", dbMode, dbmode) + return "", fmt.Errorf("failed to find %q config key in kong configuration", key) } + return value, nil } diff --git a/kong/utils_test.go b/kong/utils_test.go index 62c9d26cb..a4fde4505 100644 --- a/kong/utils_test.go +++ b/kong/utils_test.go @@ -861,6 +861,7 @@ func Test_requestWithHeaders(t *testing.T) { } func TestFillRoutesDefaults(T *testing.T) { + SkipWhenKongRouterFlavor(T, Expressions) assert := assert.New(T) client, err := NewTestClient(nil, nil) diff --git a/kong/vault_service_test.go b/kong/vault_service_test.go index a7c5712b8..40c3f95b8 100644 --- a/kong/vault_service_test.go +++ b/kong/vault_service_test.go @@ -75,7 +75,9 @@ func TestVaultsService(t *testing.T) { require.Equal(id, *createdVault.ID) require.Equal("aws", *createdVault.Name) require.Equal("aws vault for secrets", *createdVault.Description) - require.Equal(Configuration{"region": "us-east-2"}, createdVault.Config) + region, ok := createdVault.Config["region"] + require.True(ok) + require.Equal("us-east-2", region) err = client.Vaults.Delete(defaultCtx, createdVault.ID) require.NoError(err)