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/ca_certificate.go b/kong2kic/ca_certificate.go index 7cd4e92a2..d7404338e 100644 --- a/kong2kic/ca_certificate.go +++ b/kong2kic/ca_certificate.go @@ -32,7 +32,7 @@ func populateKICCACertificate(content *file.Content, file *KICContent) { continue } if caCert.CertDigest != nil { - secret.StringData["ca.digest"] = *caCert.CertDigest + secret.StringData[SecretCADigest] = *caCert.CertDigest } // add konghq.com/tags annotation if cacert.Tags is not nil @@ -43,7 +43,7 @@ func populateKICCACertificate(content *file.Content, file *KICContent) { tags = append(tags, *tag) } } - secret.ObjectMeta.Annotations["konghq.com/tags"] = strings.Join(tags, ",") + secret.ObjectMeta.Annotations[KongHQTags] = strings.Join(tags, ",") } file.Secrets = append(file.Secrets, secret) diff --git a/kong2kic/certificate.go b/kong2kic/certificate.go index 48c4e9daf..0c4d590b4 100644 --- a/kong2kic/certificate.go +++ b/kong2kic/certificate.go @@ -42,7 +42,7 @@ func populateKICCertificates(content *file.Content, file *KICContent) { tags = append(tags, *tag) } } - secret.ObjectMeta.Annotations["konghq.com/tags"] = strings.Join(tags, ",") + secret.ObjectMeta.Annotations[KongHQTags] = strings.Join(tags, ",") } file.Secrets = append(file.Secrets, secret) diff --git a/kong2kic/consumer.go b/kong2kic/consumer.go index 7ae1ee824..93824fe20 100644 --- a/kong2kic/consumer.go +++ b/kong2kic/consumer.go @@ -18,8 +18,8 @@ func populateKICConsumers(content *file.Content, file *KICContent) error { username := *consumer.Username kongConsumer := configurationv1.KongConsumer{ TypeMeta: metav1.TypeMeta{ - APIVersion: KICAPIVersion, - Kind: "KongConsumer", + APIVersion: ConfigurationKongHQv1, + Kind: KongConsumerKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(username), diff --git a/kong2kic/consumer_credentials.go b/kong2kic/consumer_credentials.go index ad4dc507d..f3bb40c2b 100644 --- a/kong2kic/consumer_credentials.go +++ b/kong2kic/consumer_credentials.go @@ -15,9 +15,9 @@ func createCredentialSecret(consumerUsername, credentialType string, dataFields stringData := make(map[string]string) labels := map[string]string{} if targetKICVersionAPI == KICV3GATEWAY || targetKICVersionAPI == KICV3INGRESS { - labels["konghq.com/credential"] = credentialType + labels[KongHQCredential] = credentialType } else { - stringData["kongCredType"] = credentialType + stringData[KongCredType] = credentialType } // Add the data fields to stringData diff --git a/kong2kic/consumer_group.go b/kong2kic/consumer_group.go index ebde749e1..1b547c0b0 100644 --- a/kong2kic/consumer_group.go +++ b/kong2kic/consumer_group.go @@ -23,8 +23,8 @@ func createConsumerGroupKongPlugin( pluginName := *plugin.Name kongPlugin := &configurationv1.KongPlugin{ TypeMeta: metav1.TypeMeta{ - APIVersion: "configuration.konghq.com/v1", - Kind: "KongPlugin", + APIVersion: ConfigurationKongHQv1, + Kind: KongPluginKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(ownerName + "-" + pluginName), @@ -55,8 +55,8 @@ func populateKICConsumerGroups(content *file.Content, kicContent *KICContent) er kongConsumerGroup := configurationv1beta1.KongConsumerGroup{ TypeMeta: metav1.TypeMeta{ - APIVersion: "configuration.konghq.com/v1beta1", - Kind: "KongConsumerGroup", + APIVersion: ConfigurationKongHQv1beta1, + Kind: KongConsumerGroupKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(groupName), diff --git a/kong2kic/global_plugin.go b/kong2kic/global_plugin.go index 266fe7c06..f4c062d5a 100644 --- a/kong2kic/global_plugin.go +++ b/kong2kic/global_plugin.go @@ -22,8 +22,8 @@ func populateKICKongClusterPlugins(content *file.Content, file *KICContent) erro continue } var kongClusterPlugin configurationv1.KongClusterPlugin - kongClusterPlugin.APIVersion = KICAPIVersion - kongClusterPlugin.Kind = "KongClusterPlugin" + kongClusterPlugin.APIVersion = ConfigurationKongHQv1 + kongClusterPlugin.Kind = KongClusterPluginKind kongClusterPlugin.ObjectMeta.Annotations = map[string]string{IngressClass: ClassName} if plugin.Name != nil { kongClusterPlugin.PluginName = *plugin.Name @@ -65,7 +65,7 @@ func populateKICKongClusterPlugins(content *file.Content, file *KICContent) erro tags = append(tags, *tag) } } - kongClusterPlugin.ObjectMeta.Annotations["konghq.com/tags"] = strings.Join(tags, ",") + kongClusterPlugin.ObjectMeta.Annotations[KongHQTags] = strings.Join(tags, ",") } // transform the plugin config from map[string]interface{} to apiextensionsv1.JSON diff --git a/kong2kic/kong2kic.go b/kong2kic/kong2kic.go index 981826a2f..ed7ecad60 100644 --- a/kong2kic/kong2kic.go +++ b/kong2kic/kong2kic.go @@ -9,22 +9,6 @@ import ( "github.com/kong/go-database-reconciler/pkg/file" ) -const ( - KICV3GATEWAY = "KICV3_GATEWAY" - KICV3INGRESS = "KICV3_INGRESS" - KICV2GATEWAY = "KICV2_GATEWAY" - KICV2INGRESS = "KICV2_INGRESS" - KICAPIVersion = "configuration.konghq.com/v1" - KICAPIVersionV1Beta1 = "configuration.konghq.com/v1beta1" - GatewayAPIVersionV1Beta1 = "gateway.networking.k8s.io/v1beta1" - GatewayAPIVersionV1 = "gateway.networking.k8s.io/v1" - KongPluginKind = "KongPlugin" - SecretKind = "Secret" - IngressKind = "KongIngress" - UpstreamPolicyKind = "KongUpstreamPolicy" - IngressClass = "kubernetes.io/ingress.class" -) - // ClassName is set by the CLI flag --class-name var ClassName = "kong" 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 7e0bc2ef8..4a91997aa 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 @@ -23,27 +26,27 @@ func addAnnotationsFromRoute(route *file.FRoute, annotations map[string]string) protocols = append(protocols, *protocol) } } - annotations["konghq.com/protocols"] = strings.Join(protocols, ",") + annotations[KongHQProtocols] = strings.Join(protocols, ",") } if route.StripPath != nil { - annotations["konghq.com/strip-path"] = strconv.FormatBool(*route.StripPath) + annotations[KongHQStripPath] = strconv.FormatBool(*route.StripPath) } if route.PreserveHost != nil { - annotations["konghq.com/preserve-host"] = strconv.FormatBool(*route.PreserveHost) + annotations[KongHQPreserveHost] = strconv.FormatBool(*route.PreserveHost) } if route.RegexPriority != nil { - annotations["konghq.com/regex-priority"] = strconv.Itoa(*route.RegexPriority) + annotations[KongHQRegexPriority] = strconv.Itoa(*route.RegexPriority) } if route.HTTPSRedirectStatusCode != nil { - annotations["konghq.com/https-redirect-status-code"] = strconv.Itoa(*route.HTTPSRedirectStatusCode) + annotations[KongHQHTTPSRedirectStatusCode] = strconv.Itoa(*route.HTTPSRedirectStatusCode) } if route.Headers != nil { for key, value := range route.Headers { - annotations["konghq.com/headers."+key] = strings.Join(value, ",") + annotations[KongHQHeaders+"."+key] = strings.Join(value, ",") } } if route.PathHandling != nil { - annotations["konghq.com/path-handling"] = *route.PathHandling + annotations[KongHQPathHandling] = *route.PathHandling } if route.SNIs != nil { var snis []string @@ -52,13 +55,13 @@ func addAnnotationsFromRoute(route *file.FRoute, annotations map[string]string) snis = append(snis, *sni) } } - annotations["konghq.com/snis"] = strings.Join(snis, ",") + annotations[KongHQSNIs] = strings.Join(snis, ",") } if route.RequestBuffering != nil { - annotations["konghq.com/request-buffering"] = strconv.FormatBool(*route.RequestBuffering) + annotations[KongHQRequestBuffering] = strconv.FormatBool(*route.RequestBuffering) } if route.ResponseBuffering != nil { - annotations["konghq.com/response-buffering"] = strconv.FormatBool(*route.ResponseBuffering) + annotations[KongHQResponseBuffering] = strconv.FormatBool(*route.ResponseBuffering) } if route.Methods != nil { var methods []string @@ -67,7 +70,8 @@ func addAnnotationsFromRoute(route *file.FRoute, annotations map[string]string) methods = append(methods, *method) } } - annotations["konghq.com/methods"] = strings.Join(methods, ",") + + annotations[KongHQMethods] = strings.Join(methods, ",") } if route.Tags != nil { var tags []string @@ -76,7 +80,7 @@ func addAnnotationsFromRoute(route *file.FRoute, annotations map[string]string) tags = append(tags, *tag) } } - annotations["konghq.com/tags"] = strings.Join(tags, ",") + annotations[KongHQTags] = strings.Join(tags, ",") } } @@ -131,8 +135,8 @@ func populateKICIngressesWithAnnotations(content *file.Content, kicContent *KICC k8sIngress := k8snetv1.Ingress{ TypeMeta: metav1.TypeMeta{ - APIVersion: "networking.k8s.io/v1", - Kind: "Ingress", + APIVersion: IngressAPIVersion, + Kind: IngressKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(serviceName + "-" + routeName), @@ -194,7 +198,7 @@ func addPluginsToRoute( pluginName := *plugin.Name kongPlugin := configurationv1.KongPlugin{ TypeMeta: metav1.TypeMeta{ - APIVersion: KICAPIVersion, + APIVersion: ConfigurationKongHQv1, Kind: KongPluginKind, }, ObjectMeta: metav1.ObjectMeta{ @@ -233,7 +237,7 @@ func addPluginsToRoute( tags = append(tags, *tag) } } - kongPlugin.ObjectMeta.Annotations["konghq.com/tags"] = strings.Join(tags, ",") + kongPlugin.ObjectMeta.Annotations[KongHQTags] = strings.Join(tags, ",") } configJSON, err := json.Marshal(plugin.Config) @@ -245,11 +249,309 @@ func addPluginsToRoute( } // Add plugin reference to ingress annotations - pluginsAnnotation := ingress.ObjectMeta.Annotations["konghq.com/plugins"] + pluginsAnnotation := ingress.ObjectMeta.Annotations[KongHQPlugins] if pluginsAnnotation == "" { - ingress.ObjectMeta.Annotations["konghq.com/plugins"] = kongPlugin.ObjectMeta.Name + ingress.ObjectMeta.Annotations[KongHQPlugins] = kongPlugin.ObjectMeta.Name } else { - ingress.ObjectMeta.Annotations["konghq.com/plugins"] = pluginsAnnotation + "," + kongPlugin.ObjectMeta.Name + ingress.ObjectMeta.Annotations[KongHQPlugins] = pluginsAnnotation + "," + kongPlugin.ObjectMeta.Name + } + + kicContent.KongPlugins = append(kicContent.KongPlugins, kongPlugin) + } + 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 = HTTPRouteKind + 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[KongHQPreserveHost] = strconv.FormatBool(*route.PreserveHost) + } + if route.StripPath != nil { + httpRoute.ObjectMeta.Annotations[KongHQStripPath] = strconv.FormatBool(*route.StripPath) + } + if route.HTTPSRedirectStatusCode != nil { + value := strconv.Itoa(*route.HTTPSRedirectStatusCode) + httpRoute.ObjectMeta.Annotations[KongHQHTTPSRedirectStatusCode] = value + } + if route.RegexPriority != nil { + httpRoute.ObjectMeta.Annotations[KongHQRegexPriority] = strconv.Itoa(*route.RegexPriority) + } + if route.PathHandling != nil { + httpRoute.ObjectMeta.Annotations[KongHQPathHandling] = *route.PathHandling + } + if route.Tags != nil { + var tags []string + for _, tag := range route.Tags { + if tag != nil { + tags = append(tags, *tag) + } + } + httpRoute.ObjectMeta.Annotations[KongHQTags] = strings.Join(tags, ",") + } + if route.SNIs != nil { + var snis string + var sb strings.Builder + for _, sni := range route.SNIs { + if sb.Len() > 0 { + sb.WriteString(",") + } + sb.WriteString(*sni) + } + snis = sb.String() + httpRoute.ObjectMeta.Annotations[KongHQSNIs] = snis + } + if route.RequestBuffering != nil { + httpRoute.ObjectMeta.Annotations[KongHQRequestBuffering] = strconv.FormatBool(*route.RequestBuffering) + } + if route.ResponseBuffering != nil { + httpRoute.ObjectMeta.Annotations[KongHQResponseBuffering] = 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), + }) +} + +// Adds backend references to the given HTTPRoute based on the provided service and route. +// It constructs BackendRef, HTTPHeaderMatch, and HTTPPathMatch objects and appends them to the HTTPRoute's rules. +// The function handles different matching types for headers and paths, and supports multiple methods for each route. +func addBackendRefs(httpRoute *k8sgwapiv1.HTTPRoute, service file.FService, route *file.FRoute) { + // make this HTTPRoute point to the service + 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 + } + + // add header match conditions to the HTTPRoute + 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], "~*") { + // it's a regular expression header match condition + httpHeaderMatch = append(httpHeaderMatch, k8sgwapiv1.HTTPHeaderMatch{ + Name: k8sgwapiv1.HTTPHeaderName(key), + Value: values[0][2:], + Type: &headerMatchRegex, + }) + } else { + // it's an exact header match condition + 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 { + // evaluate each path and method combination and add them to the HTTPRoute + for _, path := range route.Paths { + var httpPathMatch k8sgwapiv1.HTTPPathMatch + pathMatchRegex := k8sgwapiv1.PathMatchRegularExpression + pathMatchPrefix := k8sgwapiv1.PathMatchPathPrefix + + if strings.HasPrefix(*path, "~") { + // add regex path match condition + httpPathMatch.Type = &pathMatchRegex + regexPath := (*path)[1:] + httpPathMatch.Value = ®exPath + } else { + // add regular path match condition + httpPathMatch.Type = &pathMatchPrefix + httpPathMatch.Value = path + } + + if route.Methods == nil { + // this route has specific http methods to match + 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 configurationv1.KongPlugin + kongPlugin.APIVersion = ConfigurationKongHQv1 + 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: ConfigurationKongHQ, + }, + Type: k8sgwapiv1.HTTPRouteFilterExtensionRef, + }) } kicContent.KongPlugins = append(kicContent.KongPlugins, kongPlugin) diff --git a/kong2kic/service.go b/kong2kic/service.go index 756b03bb3..d096982e7 100644 --- a/kong2kic/service.go +++ b/kong2kic/service.go @@ -48,8 +48,8 @@ func populateKICServicesWithAnnotations(content *file.Content, kicContent *KICCo func createK8sService(service *file.FService, upstreams []file.FUpstream) *k8scorev1.Service { k8sService := &k8scorev1.Service{ TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", + APIVersion: ServiceAPIVersionv1, + Kind: ServiceKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(*service.Name), @@ -109,25 +109,25 @@ func isUpstreamReferenced(host *string, upstreams []file.FUpstream) bool { // Helper function to add annotations from a service to a Kubernetes service func addAnnotationsFromService(service *file.FService, annotations map[string]string) { if service.Protocol != nil { - annotations["konghq.com/protocol"] = *service.Protocol + annotations[KongHQProtocol] = *service.Protocol } if service.Path != nil { - annotations["konghq.com/path"] = *service.Path + annotations[KongHQPath] = *service.Path } if service.ClientCertificate != nil && service.ClientCertificate.ID != nil { - annotations["konghq.com/client-cert"] = *service.ClientCertificate.ID + annotations[KongHQClientCert] = *service.ClientCertificate.ID } if service.ReadTimeout != nil { - annotations["konghq.com/read-timeout"] = strconv.Itoa(*service.ReadTimeout) + annotations[KongHQReadTimeout] = strconv.Itoa(*service.ReadTimeout) } if service.WriteTimeout != nil { - annotations["konghq.com/write-timeout"] = strconv.Itoa(*service.WriteTimeout) + annotations[KongHQWriteTimeout] = strconv.Itoa(*service.WriteTimeout) } if service.ConnectTimeout != nil { - annotations["konghq.com/connect-timeout"] = strconv.Itoa(*service.ConnectTimeout) + annotations[KongHQConnectTimeout] = strconv.Itoa(*service.ConnectTimeout) } if service.Retries != nil { - annotations["konghq.com/retries"] = strconv.Itoa(*service.Retries) + annotations[KongHQRetries] = strconv.Itoa(*service.Retries) } addTagsToAnnotations(service.Tags, annotations) } 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 diff --git a/kong2kic/types.go b/kong2kic/types.go index 4ed328172..b9541143e 100644 --- a/kong2kic/types.go +++ b/kong2kic/types.go @@ -162,6 +162,11 @@ func SerializeObjectDroppingFields(obj interface{}, format string) ([]byte, erro delete(genericObj, "status") delete(genericObj["metadata"].(map[string]interface{}), "creationTimestamp") + // if genericObject has a Kind field with value "KongConsumer" then delete the field "spec" + if genericObj["kind"] == "KongConsumer" || genericObj["kind"] == "KongConsumerGroup" { + delete(genericObj, "spec") + } + if format == file.JSON { result, err = json.MarshalIndent(genericObj, "", " ") if err != nil { diff --git a/kong2kic/upstream.go b/kong2kic/upstream.go index 0611658ef..b19c79f91 100644 --- a/kong2kic/upstream.go +++ b/kong2kic/upstream.go @@ -114,7 +114,7 @@ func populateKICUpstreamPolicy( // Create KongUpstreamPolicy kongUpstreamPolicy := configurationv1beta1.KongUpstreamPolicy{ TypeMeta: metav1.TypeMeta{ - APIVersion: KICAPIVersionV1Beta1, + APIVersion: ConfigurationKongHQv1beta1, Kind: UpstreamPolicyKind, }, ObjectMeta: metav1.ObjectMeta{ @@ -127,7 +127,7 @@ func populateKICUpstreamPolicy( if k8sService.ObjectMeta.Annotations == nil { k8sService.ObjectMeta.Annotations = make(map[string]string) } - k8sService.ObjectMeta.Annotations["konghq.com/upstream-policy"] = kongUpstreamPolicy.ObjectMeta.Name + k8sService.ObjectMeta.Annotations[KongHQUpstreamPolicy] = kongUpstreamPolicy.ObjectMeta.Name // Populate the Upstream Policy Spec populateKongUpstreamPolicySpec(upstream, &kongUpstreamPolicy) @@ -204,8 +204,8 @@ func populateKICUpstream( // Create KongIngress kongIngress := configurationv1.KongIngress{ TypeMeta: metav1.TypeMeta{ - APIVersion: KICAPIVersion, - Kind: IngressKind, + APIVersion: ConfigurationKongHQv1, + Kind: KongIngressKind, }, ObjectMeta: metav1.ObjectMeta{ Name: calculateSlug(*service.Name + "-upstream"), @@ -233,7 +233,7 @@ func populateKICUpstream( if k8sService.ObjectMeta.Annotations == nil { k8sService.ObjectMeta.Annotations = make(map[string]string) } - k8sService.ObjectMeta.Annotations["konghq.com/override"] = kongIngress.ObjectMeta.Name + k8sService.ObjectMeta.Annotations[KongHQOverride] = kongIngress.ObjectMeta.Name // Add tags to annotations addTagsToAnnotations(upstream.Tags, kongIngress.ObjectMeta.Annotations) diff --git a/kong2kic/utils.go b/kong2kic/utils.go index 2e22f77b4..4c0a3f539 100644 --- a/kong2kic/utils.go +++ b/kong2kic/utils.go @@ -12,6 +12,59 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + KongHQClientCert = "konghq.com/client-cert" + KongHQConnectTimeout = "konghq.com/connect-timeout" + KongHQCredential = "konghq.com/credential" //nolint: gosec + KongHQHeaders = "konghq.com/headers" + KongHQHTTPSRedirectStatusCode = "konghq.com/https-redirect-status-code" + KongHQMethods = "konghq.com/methods" + KongHQOverride = "konghq.com/override" + KongHQPath = "konghq.com/path" + KongHQPathHandling = "konghq.com/path-handling" + KongHQPlugins = "konghq.com/plugins" + KongHQPreserveHost = "konghq.com/preserve-host" + KongHQProtocol = "konghq.com/protocol" + KongHQProtocols = "konghq.com/protocols" + KongHQReadTimeout = "konghq.com/read-timeout" + KongHQRegexPriority = "konghq.com/regex-priority" + KongHQRequestBuffering = "konghq.com/request-buffering" + KongHQResponseBuffering = "konghq.com/response-buffering" + KongHQRetries = "konghq.com/retries" + KongHQSNIs = "konghq.com/snis" + KongHQStripPath = "konghq.com/strip-path" + KongHQTags = "konghq.com/tags" + KongHQUpstreamPolicy = "konghq.com/upstream-policy" + KongHQWriteTimeout = "konghq.com/write-timeout" +) + +const ( + ConfigurationKongHQ = "configuration.konghq.com" + ConfigurationKongHQv1 = "configuration.konghq.com/v1" + ConfigurationKongHQv1beta1 = "configuration.konghq.com/v1beta1" + GatewayAPIVersionV1 = "gateway.networking.k8s.io/v1" + GatewayAPIVersionV1Beta1 = "gateway.networking.k8s.io/v1beta1" + HTTPRouteKind = "HTTPRoute" + IngressAPIVersion = "networking.k8s.io/v1" + IngressClass = "kubernetes.io/ingress.class" + IngressKind = "Ingress" + KICV2GATEWAY = "KICV2_GATEWAY" + KICV2INGRESS = "KICV2_INGRESS" + KICV3GATEWAY = "KICV3_GATEWAY" + KICV3INGRESS = "KICV3_INGRESS" + KongClusterPluginKind = "KongClusterPlugin" + KongConsumerKind = "KongConsumer" + KongConsumerGroupKind = "KongConsumerGroup" + KongCredType = "kongCredType" + KongIngressKind = "KongIngress" + KongPluginKind = "KongPlugin" + SecretKind = "Secret" + SecretCADigest = "ca.digest" + ServiceAPIVersionv1 = "v1" + ServiceKind = "Service" + UpstreamPolicyKind = "KongUpstreamPolicy" +) + // Helper function to add tags to annotations func addTagsToAnnotations(tags []*string, annotations map[string]string) { if tags != nil { @@ -45,7 +98,7 @@ func createKongPlugin(plugin *file.FPlugin, ownerName string) (*configurationv1. pluginName := *plugin.Name kongPlugin := &configurationv1.KongPlugin{ TypeMeta: metav1.TypeMeta{ - APIVersion: KICAPIVersion, + APIVersion: ConfigurationKongHQv1, Kind: KongPluginKind, }, ObjectMeta: metav1.ObjectMeta{