From c3e02c09c8d1ea72c2f9b87921a2e9ca7caa76e1 Mon Sep 17 00:00:00 2001 From: battlebyte Date: Wed, 6 Nov 2024 14:38:32 +0100 Subject: [PATCH] fix: restore Gateway API generation (issue #1427). Separate integration tests in kong2kic_integration_test.go and must be explicitly invoked with -tags=integration. Fix tests to evaluate all yaml objects. --- kong2kic/builder_v2_gw_api.go | 2 +- kong2kic/builder_v3_gw_api.go | 2 +- kong2kic/kong2kic_integration_test.go | 344 ++++++++++++++++++ kong2kic/kong2kic_test.go | 344 +----------------- kong2kic/route.go | 288 +++++++++++++++ .../testdata/consumer-v2-output-expected.yaml | 12 +- .../testdata/consumer-v3-output-expected.yaml | 12 +- 7 files changed, 664 insertions(+), 340 deletions(-) create mode 100644 kong2kic/kong2kic_integration_test.go diff --git a/kong2kic/builder_v2_gw_api.go b/kong2kic/builder_v2_gw_api.go index a30d4e21f..74b9a3db7 100644 --- a/kong2kic/builder_v2_gw_api.go +++ b/kong2kic/builder_v2_gw_api.go @@ -24,7 +24,7 @@ func (b *KICv2GatewayAPIBuilder) buildServices(content *file.Content) { } func (b *KICv2GatewayAPIBuilder) buildRoutes(content *file.Content) { - err := populateKICIngressesWithAnnotations(content, b.kicContent) + err := populateKICIngressesWithGatewayAPI(content, b.kicContent) if err != nil { log.Fatal(err) } diff --git a/kong2kic/builder_v3_gw_api.go b/kong2kic/builder_v3_gw_api.go index 81f62b8d7..4f1b6820c 100644 --- a/kong2kic/builder_v3_gw_api.go +++ b/kong2kic/builder_v3_gw_api.go @@ -24,7 +24,7 @@ func (b *KICv3GatewayAPIBuider) buildServices(content *file.Content) { } func (b *KICv3GatewayAPIBuider) buildRoutes(content *file.Content) { - err := populateKICIngressesWithAnnotations(content, b.kicContent) + err := populateKICIngressesWithGatewayAPI(content, b.kicContent) if err != nil { log.Fatal(err) } diff --git a/kong2kic/kong2kic_integration_test.go b/kong2kic/kong2kic_integration_test.go new file mode 100644 index 000000000..7471faf61 --- /dev/null +++ b/kong2kic/kong2kic_integration_test.go @@ -0,0 +1,344 @@ +//go:build integration +// +build integration + +// invoke with go test -tags=integration -run ^Test_deployManifests$ ./... +package kong2kic + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/kong" + "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/metallb" + environment "github.com/kong/kubernetes-testing-framework/pkg/environments" + "github.com/stretchr/testify/require" + apiv1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +func Test_deployManifests(t *testing.T) { + versions := []string{"2.12", "3.0", "3.1", "3.2", "3.3"} + for _, version := range versions { + t.Run("KIC Version "+version, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + + // Configure the testing environment with the specified KIC version + env, kongAddon, err := setupTestingEnvironmentWithVersion(ctx, version) + require.NoError(t, err) + defer teardownEnvironment(ctx, t, env) + + t.Log("waiting for the test environment to be ready for use") + require.NoError(t, <-env.WaitForReady(ctx)) + + t.Log("verifying the test environment becomes ready for use") + waitForObjects, ready, err := env.Ready(ctx) + require.NoError(t, err) + require.Empty(t, waitForObjects) + require.True(t, ready) + + t.Log("verifying the kong proxy is returning its default 404 response") + proxyURL, err := getKongProxyURL(ctx, env) + require.NoError(t, err) + verifyKongProxyResponse(t, proxyURL) + + t.Log("verifying that the kong addon deployed both proxy and controller") + verifyKongDeployment(ctx, t, env, kongAddon) + + config := env.Cluster().Config() + + t.Log("deploying the Gateway API CRDs") + clientset, err := deployGatewayAPICRDs(t, config) + require.NoError(t, err) + + t.Log("obtaining the ServerPreferredResources from the cluster") + kindToResource, err := getKindToResourceMap(clientset) + require.NoError(t, err) + + t.Log("creating a dynamic client for Kubernetes resources") + dynamicClient, err := dynamic.NewForConfig(config) + require.NoError(t, err) + + t.Log("deploying manifests to the cluster") + err = deployManifestsToClusterForVersion(t, dynamicClient, kindToResource, version) + require.NoError(t, err) + }) + } +} + +// Helper function to set up the testing environment with a specific KIC version +func setupTestingEnvironmentWithVersion( + ctx context.Context, + kicVersion string, +) (environment.Environment, *kong.Addon, error) { + builder := environment.NewBuilder() + kongAddonBuilder := kong.NewBuilder(). + WithControllerImage("kong/kubernetes-ingress-controller", kicVersion). + WithProxyImage("kong", "3.4") // Adjust proxy image if needed + + kongAddon := kongAddonBuilder.Build() + env, err := builder.WithAddons(metallb.New(), kongAddon).Build(ctx) + if err != nil { + return nil, nil, err + } + return env, kongAddon, nil +} + +// Mutex to avoid race condition on ~/.kube/config file +var teardownMutex sync.Mutex + +func teardownEnvironment(ctx context.Context, t *testing.T, env environment.Environment) { + // Lock the mutex to ensure only one teardown process at a time + teardownMutex.Lock() + defer teardownMutex.Unlock() + + t.Logf("cleaning up environment %s and cluster %s", env.Name(), env.Cluster().Name()) + require.NoError(t, env.Cleanup(ctx)) +} + +// Helper function to get Kong proxy URL +func getKongProxyURL(ctx context.Context, env environment.Environment) (string, error) { + kongAon, err := env.Cluster().GetAddon("kong") + if err != nil { + return "", err + } + kongAddonRaw, ok := kongAon.(*kong.Addon) + if !ok { + return "", errors.New("failed to cast kong addon") + } + proxyURL, err := kongAddonRaw.ProxyHTTPURL(ctx, env.Cluster()) + if err != nil { + return "", err + } + return proxyURL.String(), nil +} + +// Helper function to verify Kong proxy response +func verifyKongProxyResponse(t *testing.T, proxyURL string) { + httpc := http.Client{Timeout: time.Second * 10} + require.Eventually(t, func() bool { + resp, err := httpc.Get(proxyURL) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusNotFound + }, time.Minute*3, time.Second) +} + +// Helper function to verify Kong deployment +func verifyKongDeployment(ctx context.Context, t *testing.T, env environment.Environment, kongAddon *kong.Addon) { + client := env.Cluster().Client() + appsV1 := client.AppsV1() + deployments := appsV1.Deployments(kongAddon.Namespace()) + kongDeployment, err := deployments.Get(ctx, "ingress-controller-kong", metav1.GetOptions{}) + require.NoError(t, err) + require.Len(t, kongDeployment.Spec.Template.Spec.Containers, 2) + require.Equal(t, "ingress-controller", kongDeployment.Spec.Template.Spec.Containers[0].Name) + require.Equal(t, "proxy", kongDeployment.Spec.Template.Spec.Containers[1].Name) +} + +// Helper function to deploy Gateway API CRDs +func deployGatewayAPICRDs(t *testing.T, config *rest.Config) (*clientset.Clientset, error) { + clientset, err := clientset.NewForConfig(config) + if err != nil { + return nil, err + } + + gatewayAPICrdPath := filepath.Join("testdata", "gateway-api-crd.yaml") + gatewayAPICrdFile, err := os.ReadFile(gatewayAPICrdPath) + if err != nil { + return nil, err + } + + // Split the YAML file into individual documents. + yamlDocs := regexp.MustCompile(`(?m)^---\s*$`).Split(string(gatewayAPICrdFile), -1) + + for _, doc := range yamlDocs { + if strings.TrimSpace(doc) == "" { + continue + } + + dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(doc)), 4096) + var crd apiextensionsv1.CustomResourceDefinition + err := dec.Decode(&crd) + if err != nil { + return nil, err + } + + _, err = clientset.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), &crd, metav1.CreateOptions{}) + if err != nil { + return nil, err + } + t.Logf("created CRD: %s", crd.Name) + } + + // Wait for CRDs to be available + time.Sleep(2 * time.Second) + return clientset, nil +} + +// Helper function to get Kind to Resource mapping +func getKindToResourceMap(clientset *clientset.Clientset) (map[string]string, error) { + kindToResource := make(map[string]string) + groups, err := clientset.Discovery().ServerPreferredResources() + if err != nil { + return nil, err + } + for _, group := range groups { + for _, resource := range group.APIResources { + kindToResource[resource.Kind] = resource.Name + } + } + return kindToResource, nil +} + +// Helper function to deploy manifests to the cluster +func deployManifestsToClusterForVersion( + t *testing.T, + dynamicClient dynamic.Interface, + kindToResource map[string]string, + version string, +) error { + files, err := os.ReadDir("testdata/") + if err != nil { + return err + } + + for _, file := range files { + filename := file.Name() + if !strings.HasSuffix(filename, "output-expected.yaml") { + continue + } + // Skip files based on version + if version == "2.12" && strings.Contains(filename, "-v3-") { + continue + } + if version != "2.12" && strings.Contains(filename, "-v2-") { + continue + } + content, err := os.ReadFile(filepath.Join("testdata", filename)) + if err != nil { + return err + } + t.Logf("DEPLOYING MANIFEST: %s for KIC version %s", filename, version) + err = deployManifestToCluster(t, content, kindToResource, dynamicClient) + if err != nil { + return err + } + } + return nil +} + +// Simplify the deployManifestToCluster function +func deployManifestToCluster( + t *testing.T, + manifest []byte, + kindToResource map[string]string, + dynamicClient dynamic.Interface, +) error { + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 4096) + var objectsToDelete []ObjectToDelete + + for { + var rawObj unstructured.Unstructured + if err := decoder.Decode(&rawObj); err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + + gvr, err := getGroupVersionResource(&rawObj, kindToResource) + if err != nil { + return err + } + + setNamespaceIfNeeded(&rawObj) + + _, err = dynamicClient.Resource(gvr). + Namespace(rawObj.GetNamespace()). + Create(context.TODO(), &rawObj, metav1.CreateOptions{}) + if err != nil { + return err + } + t.Logf("created object: %s of Kind: %s in Namespace: %s", rawObj.GetName(), rawObj.GetKind(), rawObj.GetNamespace()) + objectsToDelete = append(objectsToDelete, ObjectToDelete{object: rawObj, gvr: gvr}) + } + + // Clean up created objects + for _, obj := range objectsToDelete { + err := dynamicClient.Resource(obj.gvr). + Namespace(obj.object.GetNamespace()). + Delete(context.TODO(), obj.object.GetName(), metav1.DeleteOptions{}) + if err != nil { + return err + } + t.Logf("deleted object: %s of Kind: %s in Namespace: %s", + obj.object.GetName(), + obj.object.GetKind(), + obj.object.GetNamespace()) + } + return nil +} + +// Helper function to get GroupVersionResource from an unstructured object +func getGroupVersionResource( + obj *unstructured.Unstructured, + kindToResource map[string]string, +) (schema.GroupVersionResource, error) { + apiVersion := obj.GetAPIVersion() + kind := obj.GetKind() + resource, exists := kindToResource[kind] + if !exists { + return schema.GroupVersionResource{}, errors.New("resource not found for kind: " + kind) + } + + parts := strings.Split(apiVersion, "/") + if len(parts) == 2 { + return schema.GroupVersionResource{ + Group: parts[0], + Version: parts[1], + Resource: resource, + }, nil + } else if len(parts) == 1 { + return schema.GroupVersionResource{ + Group: "", + Version: parts[0], + Resource: resource, + }, nil + } + return schema.GroupVersionResource{}, errors.New("invalid apiVersion: " + apiVersion) +} + +// Helper function to set namespace if needed +func setNamespaceIfNeeded(obj *unstructured.Unstructured) { + if obj.GetKind() == "KongClusterPlugin" { + obj.SetNamespace(apiv1.NamespaceAll) + } else if obj.GetNamespace() == "" { + obj.SetNamespace(apiv1.NamespaceDefault) + } +} + +// Type definition for objects to delete +type ObjectToDelete struct { + object unstructured.Unstructured + gvr schema.GroupVersionResource +} diff --git a/kong2kic/kong2kic_test.go b/kong2kic/kong2kic_test.go index a7ca821b6..4b6d08d59 100644 --- a/kong2kic/kong2kic_test.go +++ b/kong2kic/kong2kic_test.go @@ -1,33 +1,14 @@ package kong2kic import ( - "bytes" - "context" - "errors" - "io" - "net/http" "os" "path/filepath" "regexp" "strings" - "sync" "testing" - "time" "github.com/kong/go-database-reconciler/pkg/file" - "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/kong" - "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/metallb" - environment "github.com/kong/kubernetes-testing-framework/pkg/environments" "github.com/stretchr/testify/require" - apiv1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" ) var baseLocation = "testdata/" @@ -61,7 +42,20 @@ func compareFileContent(t *testing.T, expectedFilename string, actualContent []b actualJSON := "[" + strings.ReplaceAll(string(actualContent), "}{", "},{") + "]" require.JSONEq(t, expectedJSON, actualJSON) } else { - require.YAMLEq(t, string(expectedContent), string(actualContent)) + // Split the content into individual YAML documents using regex + re := regexp.MustCompile(`(?m)^---\s*$`) + expectedYAMLs := re.Split(string(expectedContent), -1) + actualYAMLs := re.Split(string(actualContent), -1) + + // Ensure both have the same number of YAML documents + require.Equal(t, len(expectedYAMLs), len(actualYAMLs), "number of YAML documents do not match") + + // Compare each YAML document + for i := range expectedYAMLs { + expectedYAML := strings.TrimSpace(expectedYAMLs[i]) + actualYAML := strings.TrimSpace(actualYAMLs[i]) + require.YAMLEq(t, expectedYAML, actualYAML, "YAML document %d does not match", i+1) + } } } @@ -198,313 +192,3 @@ func Test_convertKongGatewayToKIC(t *testing.T) { }) } } - -func Test_deployManifests(t *testing.T) { - versions := []string{"2.12", "3.0", "3.1", "3.2", "3.3"} - for _, version := range versions { - t.Run("KIC Version "+version, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - - // Configure the testing environment with the specified KIC version - env, kongAddon, err := setupTestingEnvironmentWithVersion(ctx, version) - require.NoError(t, err) - defer teardownEnvironment(ctx, t, env) - - t.Log("waiting for the test environment to be ready for use") - require.NoError(t, <-env.WaitForReady(ctx)) - - t.Log("verifying the test environment becomes ready for use") - waitForObjects, ready, err := env.Ready(ctx) - require.NoError(t, err) - require.Empty(t, waitForObjects) - require.True(t, ready) - - t.Log("verifying the kong proxy is returning its default 404 response") - proxyURL, err := getKongProxyURL(ctx, env) - require.NoError(t, err) - verifyKongProxyResponse(t, proxyURL) - - t.Log("verifying that the kong addon deployed both proxy and controller") - verifyKongDeployment(ctx, t, env, kongAddon) - - config := env.Cluster().Config() - - t.Log("deploying the Gateway API CRDs") - clientset, err := deployGatewayAPICRDs(t, config) - require.NoError(t, err) - - t.Log("obtaining the ServerPreferredResources from the cluster") - kindToResource, err := getKindToResourceMap(clientset) - require.NoError(t, err) - - t.Log("creating a dynamic client for Kubernetes resources") - dynamicClient, err := dynamic.NewForConfig(config) - require.NoError(t, err) - - t.Log("deploying manifests to the cluster") - err = deployManifestsToClusterForVersion(t, dynamicClient, kindToResource, version) - require.NoError(t, err) - }) - } -} - -// Helper function to set up the testing environment with a specific KIC version -func setupTestingEnvironmentWithVersion( - ctx context.Context, - kicVersion string, -) (environment.Environment, *kong.Addon, error) { - builder := environment.NewBuilder() - kongAddonBuilder := kong.NewBuilder(). - WithControllerImage("kong/kubernetes-ingress-controller", kicVersion). - WithProxyImage("kong", "3.4") // Adjust proxy image if needed - - kongAddon := kongAddonBuilder.Build() - env, err := builder.WithAddons(metallb.New(), kongAddon).Build(ctx) - if err != nil { - return nil, nil, err - } - return env, kongAddon, nil -} - -// Mutex to avoid race condition on ~/.kube/config file -var teardownMutex sync.Mutex - -func teardownEnvironment(ctx context.Context, t *testing.T, env environment.Environment) { - // Lock the mutex to ensure only one teardown process at a time - teardownMutex.Lock() - defer teardownMutex.Unlock() - - t.Logf("cleaning up environment %s and cluster %s", env.Name(), env.Cluster().Name()) - require.NoError(t, env.Cleanup(ctx)) -} - -// Helper function to get Kong proxy URL -func getKongProxyURL(ctx context.Context, env environment.Environment) (string, error) { - kongAon, err := env.Cluster().GetAddon("kong") - if err != nil { - return "", err - } - kongAddonRaw, ok := kongAon.(*kong.Addon) - if !ok { - return "", errors.New("failed to cast kong addon") - } - proxyURL, err := kongAddonRaw.ProxyHTTPURL(ctx, env.Cluster()) - if err != nil { - return "", err - } - return proxyURL.String(), nil -} - -// Helper function to verify Kong proxy response -func verifyKongProxyResponse(t *testing.T, proxyURL string) { - httpc := http.Client{Timeout: time.Second * 10} - require.Eventually(t, func() bool { - resp, err := httpc.Get(proxyURL) - if err != nil { - return false - } - defer resp.Body.Close() - return resp.StatusCode == http.StatusNotFound - }, time.Minute*3, time.Second) -} - -// Helper function to verify Kong deployment -func verifyKongDeployment(ctx context.Context, t *testing.T, env environment.Environment, kongAddon *kong.Addon) { - client := env.Cluster().Client() - appsV1 := client.AppsV1() - deployments := appsV1.Deployments(kongAddon.Namespace()) - kongDeployment, err := deployments.Get(ctx, "ingress-controller-kong", metav1.GetOptions{}) - require.NoError(t, err) - require.Len(t, kongDeployment.Spec.Template.Spec.Containers, 2) - require.Equal(t, "ingress-controller", kongDeployment.Spec.Template.Spec.Containers[0].Name) - require.Equal(t, "proxy", kongDeployment.Spec.Template.Spec.Containers[1].Name) -} - -// Helper function to deploy Gateway API CRDs -func deployGatewayAPICRDs(t *testing.T, config *rest.Config) (*clientset.Clientset, error) { - clientset, err := clientset.NewForConfig(config) - if err != nil { - return nil, err - } - - gatewayAPICrdPath := filepath.Join("testdata", "gateway-api-crd.yaml") - gatewayAPICrdFile, err := os.ReadFile(gatewayAPICrdPath) - if err != nil { - return nil, err - } - - // Split the YAML file into individual documents. - yamlDocs := regexp.MustCompile(`(?m)^---\s*$`).Split(string(gatewayAPICrdFile), -1) - - for _, doc := range yamlDocs { - if strings.TrimSpace(doc) == "" { - continue - } - - dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(doc)), 4096) - var crd apiextensionsv1.CustomResourceDefinition - err := dec.Decode(&crd) - if err != nil { - return nil, err - } - - _, err = clientset.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), &crd, metav1.CreateOptions{}) - if err != nil { - return nil, err - } - t.Logf("created CRD: %s", crd.Name) - } - - // Wait for CRDs to be available - time.Sleep(2 * time.Second) - return clientset, nil -} - -// Helper function to get Kind to Resource mapping -func getKindToResourceMap(clientset *clientset.Clientset) (map[string]string, error) { - kindToResource := make(map[string]string) - groups, err := clientset.Discovery().ServerPreferredResources() - if err != nil { - return nil, err - } - for _, group := range groups { - for _, resource := range group.APIResources { - kindToResource[resource.Kind] = resource.Name - } - } - return kindToResource, nil -} - -// Helper function to deploy manifests to the cluster -func deployManifestsToClusterForVersion( - t *testing.T, - dynamicClient dynamic.Interface, - kindToResource map[string]string, - version string, -) error { - files, err := os.ReadDir("testdata/") - if err != nil { - return err - } - - for _, file := range files { - filename := file.Name() - if !strings.HasSuffix(filename, "output-expected.yaml") { - continue - } - // Skip files based on version - if version == "2.12" && strings.Contains(filename, "-v3-") { - continue - } - if version != "2.12" && strings.Contains(filename, "-v2-") { - continue - } - content, err := os.ReadFile(filepath.Join("testdata", filename)) - if err != nil { - return err - } - t.Logf("DEPLOYING MANIFEST: %s for KIC version %s", filename, version) - err = deployManifestToCluster(t, content, kindToResource, dynamicClient) - if err != nil { - return err - } - } - return nil -} - -// Simplify the deployManifestToCluster function -func deployManifestToCluster( - t *testing.T, - manifest []byte, - kindToResource map[string]string, - dynamicClient dynamic.Interface, -) error { - decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 4096) - var objectsToDelete []ObjectToDelete - - for { - var rawObj unstructured.Unstructured - if err := decoder.Decode(&rawObj); err != nil { - if errors.Is(err, io.EOF) { - break - } - return err - } - - gvr, err := getGroupVersionResource(&rawObj, kindToResource) - if err != nil { - return err - } - - setNamespaceIfNeeded(&rawObj) - - _, err = dynamicClient.Resource(gvr). - Namespace(rawObj.GetNamespace()). - Create(context.TODO(), &rawObj, metav1.CreateOptions{}) - if err != nil { - return err - } - t.Logf("created object: %s of Kind: %s in Namespace: %s", rawObj.GetName(), rawObj.GetKind(), rawObj.GetNamespace()) - objectsToDelete = append(objectsToDelete, ObjectToDelete{object: rawObj, gvr: gvr}) - } - - // Clean up created objects - for _, obj := range objectsToDelete { - err := dynamicClient.Resource(obj.gvr). - Namespace(obj.object.GetNamespace()). - Delete(context.TODO(), obj.object.GetName(), metav1.DeleteOptions{}) - if err != nil { - return err - } - t.Logf("deleted object: %s of Kind: %s in Namespace: %s", - obj.object.GetName(), - obj.object.GetKind(), - obj.object.GetNamespace()) - } - return nil -} - -// Helper function to get GroupVersionResource from an unstructured object -func getGroupVersionResource( - obj *unstructured.Unstructured, - kindToResource map[string]string, -) (schema.GroupVersionResource, error) { - apiVersion := obj.GetAPIVersion() - kind := obj.GetKind() - resource, exists := kindToResource[kind] - if !exists { - return schema.GroupVersionResource{}, errors.New("resource not found for kind: " + kind) - } - - parts := strings.Split(apiVersion, "/") - if len(parts) == 2 { - return schema.GroupVersionResource{ - Group: parts[0], - Version: parts[1], - Resource: resource, - }, nil - } else if len(parts) == 1 { - return schema.GroupVersionResource{ - Group: "", - Version: parts[0], - Resource: resource, - }, nil - } - return schema.GroupVersionResource{}, errors.New("invalid apiVersion: " + apiVersion) -} - -// Helper function to set namespace if needed -func setNamespaceIfNeeded(obj *unstructured.Unstructured) { - if obj.GetKind() == "KongClusterPlugin" { - obj.SetNamespace(apiv1.NamespaceAll) - } else if obj.GetNamespace() == "" { - obj.SetNamespace(apiv1.NamespaceDefault) - } -} - -// Type definition for objects to delete -type ObjectToDelete struct { - object unstructured.Unstructured - gvr schema.GroupVersionResource -} diff --git a/kong2kic/route.go b/kong2kic/route.go index 0c014c7ea..52b71843b 100644 --- a/kong2kic/route.go +++ b/kong2kic/route.go @@ -2,7 +2,9 @@ package kong2kic import ( "encoding/json" + "errors" "log" + "sort" "strconv" "strings" @@ -12,6 +14,7 @@ import ( k8snetv1 "k8s.io/api/networking/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sgwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) // Helper function to add annotations from a route to an ingress @@ -257,3 +260,288 @@ func addPluginsToRoute( } return nil } + +// HeadersByNameTypeAndValue is a type to sort headers by name, type and value. +// This is needed to ensure that the order of headers is consistent across runs. +type HeadersByNameTypeAndValue []k8sgwapiv1.HTTPHeaderMatch + +func (a HeadersByNameTypeAndValue) Len() int { return len(a) } +func (a HeadersByNameTypeAndValue) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a HeadersByNameTypeAndValue) Less(i, j int) bool { + if a[i].Name < a[j].Name { + return true + } + if a[i].Name > a[j].Name { + return false + } + + if a[i].Type != nil && a[j].Type == nil { + return true + } + if a[i].Type == nil && a[j].Type != nil { + return false + } + + if *a[i].Type < *a[j].Type { + return true + } + if *a[i].Type > *a[j].Type { + return false + } + return a[i].Value < a[j].Value +} + +// Convert route to HTTPRoute (Gateway API) +func populateKICIngressesWithGatewayAPI(content *file.Content, kicContent *KICContent) error { + for _, service := range content.Services { + for _, route := range service.Routes { + httpRoute, err := createHTTPRoute(service, route) + if err != nil { + log.Println(err) + continue + } + + err = addPluginsToGatewayAPIRoute(service, route, httpRoute, kicContent) + if err != nil { + return err + } + kicContent.HTTPRoutes = append(kicContent.HTTPRoutes, httpRoute) + } + } + return nil +} + +func createHTTPRoute(service file.FService, route *file.FRoute) (k8sgwapiv1.HTTPRoute, error) { + var httpRoute k8sgwapiv1.HTTPRoute + httpRoute.Kind = "HTTPRoute" + if targetKICVersionAPI == KICV3GATEWAY { + httpRoute.APIVersion = GatewayAPIVersionV1 + } else { + httpRoute.APIVersion = GatewayAPIVersionV1Beta1 + } + if service.Name != nil && route.Name != nil { + httpRoute.ObjectMeta.Name = calculateSlug(*service.Name + "-" + *route.Name) + } else { + return httpRoute, errors.New( + "service name or route name is empty. Please provide a name for the service" + + "and the route before generating HTTPRoute manifests", + ) + } + httpRoute.ObjectMeta.Annotations = make(map[string]string) + + addAnnotations(&httpRoute, route) + addHosts(&httpRoute, route) + addParentRefs(&httpRoute) + addBackendRefs(&httpRoute, service, route) + + return httpRoute, nil +} + +func addAnnotations(httpRoute *k8sgwapiv1.HTTPRoute, route *file.FRoute) { + if route.PreserveHost != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/preserve-host"] = strconv.FormatBool(*route.PreserveHost) + } + if route.StripPath != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/strip-path"] = strconv.FormatBool(*route.StripPath) + } + if route.HTTPSRedirectStatusCode != nil { + value := strconv.Itoa(*route.HTTPSRedirectStatusCode) + httpRoute.ObjectMeta.Annotations["konghq.com/https-redirect-status-code"] = value + } + if route.RegexPriority != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/regex-priority"] = strconv.Itoa(*route.RegexPriority) + } + if route.PathHandling != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/path-handling"] = *route.PathHandling + } + if route.Tags != nil { + var tags []string + for _, tag := range route.Tags { + if tag != nil { + tags = append(tags, *tag) + } + } + httpRoute.ObjectMeta.Annotations["konghq.com/tags"] = strings.Join(tags, ",") + } + if route.SNIs != nil { + var snis string + for _, sni := range route.SNIs { + if snis == "" { + snis = *sni + } else { + snis = snis + "," + *sni + } + } + httpRoute.ObjectMeta.Annotations["konghq.com/snis"] = snis + } + if route.RequestBuffering != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/request-buffering"] = strconv.FormatBool(*route.RequestBuffering) + } + if route.ResponseBuffering != nil { + httpRoute.ObjectMeta.Annotations["konghq.com/response-buffering"] = strconv.FormatBool(*route.ResponseBuffering) + } +} + +func addHosts(httpRoute *k8sgwapiv1.HTTPRoute, route *file.FRoute) { + if route.Hosts != nil { + for _, host := range route.Hosts { + httpRoute.Spec.Hostnames = append(httpRoute.Spec.Hostnames, k8sgwapiv1.Hostname(*host)) + } + } +} + +func addParentRefs(httpRoute *k8sgwapiv1.HTTPRoute) { + httpRoute.Spec.ParentRefs = append(httpRoute.Spec.ParentRefs, k8sgwapiv1.ParentReference{ + Name: k8sgwapiv1.ObjectName(ClassName), + }) +} + +func addBackendRefs(httpRoute *k8sgwapiv1.HTTPRoute, service file.FService, route *file.FRoute) { + backendRef := k8sgwapiv1.BackendRef{ + BackendObjectReference: k8sgwapiv1.BackendObjectReference{ + Name: k8sgwapiv1.ObjectName(*service.Name), + }, + } + if service.Port != nil { + portNumber := k8sgwapiv1.PortNumber(*service.Port) //nolint:gosec + backendRef.Port = &portNumber + } + + var httpHeaderMatch []k8sgwapiv1.HTTPHeaderMatch + headerMatchExact := k8sgwapiv1.HeaderMatchExact + headerMatchRegex := k8sgwapiv1.HeaderMatchRegularExpression + if route.Headers != nil { + for key, values := range route.Headers { + if len(values) == 1 && strings.HasPrefix(values[0], "~*") { + httpHeaderMatch = append(httpHeaderMatch, k8sgwapiv1.HTTPHeaderMatch{ + Name: k8sgwapiv1.HTTPHeaderName(key), + Value: values[0][2:], + Type: &headerMatchRegex, + }) + } else { + var value string + if len(values) > 1 { + value = strings.Join(values, ",") + } else { + value = values[0] + } + httpHeaderMatch = append(httpHeaderMatch, k8sgwapiv1.HTTPHeaderMatch{ + Name: k8sgwapiv1.HTTPHeaderName(key), + Value: value, + Type: &headerMatchExact, + }) + } + } + sort.Sort(HeadersByNameTypeAndValue(httpHeaderMatch)) + } + + if route.Paths != nil { + for _, path := range route.Paths { + var httpPathMatch k8sgwapiv1.HTTPPathMatch + pathMatchRegex := k8sgwapiv1.PathMatchRegularExpression + pathMatchPrefix := k8sgwapiv1.PathMatchPathPrefix + + if strings.HasPrefix(*path, "~") { + httpPathMatch.Type = &pathMatchRegex + regexPath := (*path)[1:] + httpPathMatch.Value = ®exPath + } else { + httpPathMatch.Type = &pathMatchPrefix + httpPathMatch.Value = path + } + + if route.Methods == nil { + httpRoute.Spec.Rules = append(httpRoute.Spec.Rules, k8sgwapiv1.HTTPRouteRule{ + Matches: []k8sgwapiv1.HTTPRouteMatch{ + { + Path: &httpPathMatch, + Headers: httpHeaderMatch, + }, + }, + BackendRefs: []k8sgwapiv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, + }) + } + + for _, method := range route.Methods { + httpMethod := k8sgwapiv1.HTTPMethod(*method) + httpRoute.Spec.Rules = append(httpRoute.Spec.Rules, k8sgwapiv1.HTTPRouteRule{ + Matches: []k8sgwapiv1.HTTPRouteMatch{ + { + Path: &httpPathMatch, + Method: &httpMethod, + Headers: httpHeaderMatch, + }, + }, + BackendRefs: []k8sgwapiv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, + }) + } + } + } else { + for _, method := range route.Methods { + httpMethod := k8sgwapiv1.HTTPMethod(*method) + httpRoute.Spec.Rules = append(httpRoute.Spec.Rules, k8sgwapiv1.HTTPRouteRule{ + Matches: []k8sgwapiv1.HTTPRouteMatch{ + { + Method: &httpMethod, + Headers: httpHeaderMatch, + }, + }, + BackendRefs: []k8sgwapiv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, + }) + } + } +} + +func addPluginsToGatewayAPIRoute( + service file.FService, route *file.FRoute, httpRoute k8sgwapiv1.HTTPRoute, kicContent *KICContent, +) error { + for _, plugin := range route.Plugins { + var kongPlugin kicv1.KongPlugin + kongPlugin.APIVersion = KICAPIVersion + kongPlugin.Kind = KongPluginKind + if plugin.Name != nil && route.Name != nil && service.Name != nil { + kongPlugin.ObjectMeta.Name = calculateSlug(*service.Name + "-" + *route.Name + "-" + *plugin.Name) + } else { + log.Println("Service name, route name or plugin name is empty. This is not recommended." + + "Please, provide a name for the service, route and the plugin before generating Kong Ingress Controller manifests.") + continue + } + kongPlugin.ObjectMeta.Annotations = map[string]string{IngressClass: ClassName} + kongPlugin.PluginName = *plugin.Name + + var configJSON apiextensionsv1.JSON + var err error + configJSON.Raw, err = json.Marshal(plugin.Config) + if err != nil { + return err + } + kongPlugin.Config = configJSON + + // add plugins as extensionRef under filters for every rule + for i := range httpRoute.Spec.Rules { + httpRoute.Spec.Rules[i].Filters = append(httpRoute.Spec.Rules[i].Filters, k8sgwapiv1.HTTPRouteFilter{ + ExtensionRef: &k8sgwapiv1.LocalObjectReference{ + Name: k8sgwapiv1.ObjectName(kongPlugin.ObjectMeta.Name), + Kind: KongPluginKind, + Group: "configuration.konghq.com", + }, + Type: k8sgwapiv1.HTTPRouteFilterExtensionRef, + }) + } + + kicContent.KongPlugins = append(kicContent.KongPlugins, kongPlugin) + } + return nil +} diff --git a/kong2kic/testdata/consumer-v2-output-expected.yaml b/kong2kic/testdata/consumer-v2-output-expected.yaml index 9978ab114..56f7c36f6 100644 --- a/kong2kic/testdata/consumer-v2-output-expected.yaml +++ b/kong2kic/testdata/consumer-v2-output-expected.yaml @@ -19,13 +19,14 @@ metadata: stringData: key: my_api_key kongCredType: key-auth +type: Opaque --- apiVersion: v1 kind: Secret metadata: annotations: kubernetes.io/ingress.class: kong - name: jwt-auth-example-user + name: jwt-example-user stringData: algorithm: HS256 key: my_jwt_secret @@ -41,6 +42,7 @@ stringData: ewIDAQAB -----END PUBLIC KEY----- secret: my_secret_key +type: Opaque --- apiVersion: v1 kind: Secret @@ -52,16 +54,18 @@ stringData: kongCredType: basic-auth password: my_basic_password username: my_basic_user +type: Opaque --- apiVersion: v1 kind: Secret metadata: annotations: kubernetes.io/ingress.class: kong - name: acl-group-example-user + name: acl-example-user stringData: group: acl_group kongCredType: acl +type: Opaque --- apiVersion: v1 kind: Secret @@ -78,9 +82,9 @@ type: Opaque apiVersion: configuration.konghq.com/v1 credentials: - key-auth-example-user -- jwt-auth-example-user +- jwt-example-user - basic-auth-example-user -- acl-group-example-user +- acl-example-user - mtls-auth-example-user custom_id: "1234567890" kind: KongConsumer diff --git a/kong2kic/testdata/consumer-v3-output-expected.yaml b/kong2kic/testdata/consumer-v3-output-expected.yaml index 6e843caad..5e7ae9150 100644 --- a/kong2kic/testdata/consumer-v3-output-expected.yaml +++ b/kong2kic/testdata/consumer-v3-output-expected.yaml @@ -20,6 +20,7 @@ metadata: name: key-auth-example-user stringData: key: my_api_key +type: Opaque --- apiVersion: v1 kind: Secret @@ -28,7 +29,7 @@ metadata: kubernetes.io/ingress.class: kong labels: konghq.com/credential: jwt - name: jwt-auth-example-user + name: jwt-example-user stringData: algorithm: HS256 key: my_jwt_secret @@ -43,6 +44,7 @@ stringData: ewIDAQAB -----END PUBLIC KEY----- secret: my_secret_key +type: Opaque --- apiVersion: v1 kind: Secret @@ -55,6 +57,7 @@ metadata: stringData: password: my_basic_password username: my_basic_user +type: Opaque --- apiVersion: v1 kind: Secret @@ -63,9 +66,10 @@ metadata: kubernetes.io/ingress.class: kong labels: konghq.com/credential: acl - name: acl-group-example-user + name: acl-example-user stringData: group: acl_group +type: Opaque --- apiVersion: v1 kind: Secret @@ -83,9 +87,9 @@ type: Opaque apiVersion: configuration.konghq.com/v1 credentials: - key-auth-example-user -- jwt-auth-example-user +- jwt-example-user - basic-auth-example-user -- acl-group-example-user +- acl-example-user - mtls-auth-example-user custom_id: "1234567890" kind: KongConsumer