diff --git a/.gitignore b/.gitignore index cd75da4de..7a1f1d585 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ docs/cli-docs/ # generated test 'actuals' kong2kic/testdata/**/*-actual.* +kong2tf/testdata/**/*-actual.* +kong2tf/terraform diff --git a/cmd/file_kong2tf.go b/cmd/file_kong2tf.go new file mode 100644 index 000000000..5fbae1c15 --- /dev/null +++ b/cmd/file_kong2tf.go @@ -0,0 +1,95 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/kong/deck/kong2tf" + "github.com/kong/go-apiops/filebasics" + "github.com/kong/go-apiops/logbasics" + "github.com/kong/go-database-reconciler/pkg/file" + "github.com/spf13/cobra" +) + +var ( + cmdKong2TfInputFilename string + cmdKong2TfOutputFilename string + cmdKong2TfGenerateImportsForControlPlaneID string + cmdKong2TfIgnoreCredentialChanges bool +) + +// Executes the CLI command "kong2Tf" +func executeKong2Tf(cmd *cobra.Command, _ []string) error { + _ = sendAnalytics("file-kong2Tf", "", modeLocal) + var ( + result string + err error + ) + + verbosity, _ := cmd.Flags().GetInt("verbose") + logbasics.Initialize(log.LstdFlags, verbosity) + + logbasics.Info("Starting execution of executeKong2Tf") + + inputContent, err := file.GetContentFromFiles([]string{cmdKong2TfInputFilename}, false) + if err != nil { + log.Printf("Error reading input file '%s'; %v", cmdKong2TfInputFilename, err) + return fmt.Errorf("failed reading input file '%s'; %w", cmdKong2TfInputFilename, err) + } + logbasics.Info("Successfully read input file '%s'", cmdKong2TfInputFilename) + + logbasics.Info("Converting Kong configuration to Terraform") + + var generateImportsForControlPlaneID *string + if cmdKong2TfGenerateImportsForControlPlaneID != "" { + generateImportsForControlPlaneID = &cmdKong2TfGenerateImportsForControlPlaneID + } + result, err = kong2tf.Convert(inputContent, generateImportsForControlPlaneID, cmdKong2TfIgnoreCredentialChanges) + if err != nil { + log.Printf("Error converting Kong configuration to Terraform; %v", err) + return fmt.Errorf("failed converting Kong configuration to Terraform; %w", err) + } + logbasics.Info("Successfully converted Kong configuration to Terraform") + + logbasics.Info("Writing output to file '%s'", cmdKong2TfOutputFilename) + err = filebasics.WriteFile(cmdKong2TfOutputFilename, []byte(result)) + if err != nil { + log.Printf("Error writing output to file '%s'; %v", cmdKong2TfOutputFilename, err) + return err + } + logbasics.Info("Successfully wrote output to file '%s'", cmdKong2TfOutputFilename) + + logbasics.Info("Finished execution of executeKong2Tf") + return nil +} + +// +// +// Define the CLI data for the kong2Tf command +// +// + +func newKong2TfCmd() *cobra.Command { + kong2TfCmd := &cobra.Command{ + Use: "kong2tf", + Short: "Convert Kong configuration files to Terraform resources", + Long: `Convert Kong configuration files to Terraform resources. + +The kong2tf subcommand transforms Kong Gateway entities in deck format, +into Terraform resources.`, + RunE: executeKong2Tf, + Args: cobra.NoArgs, + } + + kong2TfCmd.Flags().StringVarP(&cmdKong2TfInputFilename, "state", "s", "-", + "decK file to process. Use - to read from stdin.") + kong2TfCmd.Flags().StringVarP(&cmdKong2TfOutputFilename, "output-file", "o", "-", + "Output file to write. Use - to write to stdout.") + kong2TfCmd.Flags().StringVarP(&cmdKong2TfGenerateImportsForControlPlaneID, + "generate-imports-for-control-plane-id", "g", "", "Generate terraform import statements for the control plane ID.") + kong2TfCmd.Flags().BoolVar(&cmdKong2TfIgnoreCredentialChanges, "ignore-credential-changes", false, + "Enable flag to add a 'lifecycle' block to each consumer credential, "+ + "that ignores any changes from local to remote state.") + + return kong2TfCmd +} diff --git a/cmd/root.go b/cmd/root.go index fee3bef69..6db964918 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -253,6 +253,7 @@ It can be used to export, import, or sync entities to Kong.`, fileCmd.AddCommand(newConvertCmd(false)) fileCmd.AddCommand(newValidateCmd(false, false)) // file-based validation fileCmd.AddCommand(newKong2KicCmd()) + fileCmd.AddCommand(newKong2TfCmd()) } return rootCmd } diff --git a/go.mod b/go.mod index 626632de5..9d92585ad 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/hashstructure v1.1.0 github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index 9b2a4448b..12b83d268 100644 --- a/go.sum +++ b/go.sum @@ -218,6 +218,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= +github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/kong2tf/builder.go b/kong2tf/builder.go new file mode 100644 index 000000000..7f4666cb9 --- /dev/null +++ b/kong2tf/builder.go @@ -0,0 +1,51 @@ +package kong2tf + +import ( + "github.com/kong/go-database-reconciler/pkg/file" +) + +type ITerraformBuilder interface { + buildControlPlaneVar(*string) + buildServices(*file.Content, *string) + buildRoutes(*file.Content, *string) + buildGlobalPlugins(*file.Content, *string) + buildConsumers(*file.Content, *string, bool) + buildConsumerGroups(*file.Content, *string) + buildUpstreams(*file.Content, *string) + buildCACertificates(*file.Content, *string) + buildCertificates(*file.Content, *string) + buildVaults(*file.Content, *string) + getContent() string +} + +func getTerraformBuilder() ITerraformBuilder { + return newDefaultTerraformBuilder() +} + +type Director struct { + builder ITerraformBuilder +} + +func newDirector(builder ITerraformBuilder) *Director { + return &Director{ + builder: builder, + } +} + +func (d *Director) builTerraformResources( + content *file.Content, + generateImportsForControlPlaneID *string, + ignoreCredentialChanges bool, +) string { + d.builder.buildControlPlaneVar(generateImportsForControlPlaneID) + d.builder.buildGlobalPlugins(content, generateImportsForControlPlaneID) + d.builder.buildServices(content, generateImportsForControlPlaneID) + d.builder.buildUpstreams(content, generateImportsForControlPlaneID) + d.builder.buildRoutes(content, generateImportsForControlPlaneID) + d.builder.buildConsumers(content, generateImportsForControlPlaneID, ignoreCredentialChanges) + d.builder.buildConsumerGroups(content, generateImportsForControlPlaneID) + d.builder.buildCACertificates(content, generateImportsForControlPlaneID) + d.builder.buildCertificates(content, generateImportsForControlPlaneID) + d.builder.buildVaults(content, generateImportsForControlPlaneID) + return d.builder.getContent() +} diff --git a/kong2tf/builder_default_terraform.go b/kong2tf/builder_default_terraform.go new file mode 100644 index 000000000..fb11d791f --- /dev/null +++ b/kong2tf/builder_default_terraform.go @@ -0,0 +1,441 @@ +package kong2tf + +import ( + "crypto/md5" //nolint:gosec + "encoding/json" + "fmt" + "log" + "strings" + + "github.com/kong/go-database-reconciler/pkg/file" +) + +type DefaultTerraformBuider struct { + content string +} + +func newDefaultTerraformBuilder() *DefaultTerraformBuider { + return &DefaultTerraformBuider{} +} + +// Generic function that takes type T and returns map[string]any using JSON marshalling +func toMapAny(resource any) map[string]any { + resourceMap := make(map[string]interface{}) + resourceJSON, err := json.Marshal(resource) + if err != nil { + log.Fatal(err, "Failed to marshal resource") + return resourceMap + } + err = json.Unmarshal(resourceJSON, &resourceMap) + if err != nil { + log.Fatal(err, "Failed to unmarshal resource") + return resourceMap + } + return resourceMap +} + +func (b *DefaultTerraformBuider) buildControlPlaneVar(controlPlaneID *string) { + cpID := "YOUR_CONTROL_PLANE_ID" + if controlPlaneID != nil { + cpID = *controlPlaneID + } + b.content += fmt.Sprintf(`variable "control_plane_id" { + type = "string" + default = "%s" +}`, cpID) + "\n\n" +} + +func (b *DefaultTerraformBuider) buildServices(content *file.Content, controlPlaneID *string) { + for _, service := range content.Services { + parentResourceName := strings.ReplaceAll(*service.Name, "-", "_") + b.content += generateResource( + "gateway_service", + parentResourceName, + toMapAny(service), + map[string]string{}, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": service.ID, + }, + }, + []string{}, + ) + + for _, route := range service.Routes { + resourceName := strings.ReplaceAll(*route.Name, "-", "_") + b.content += generateResource("gateway_route", resourceName, toMapAny(route), map[string]string{ + "service": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": route.ID, + }, + }, []string{}) + + for _, plugin := range route.Plugins { + pluginName := strings.ReplaceAll(*plugin.Name, "-", "_") + b.content += generateResource("gateway_plugin", pluginName, toMapAny(plugin), map[string]string{ + "route": resourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": plugin.ID, + }, + }, []string{}) + } + } + + for _, plugin := range service.Plugins { + resourceName := strings.ReplaceAll(*plugin.Name, "-", "_") + b.content += generateResource("gateway_plugin", resourceName, toMapAny(plugin), map[string]string{ + "service": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": plugin.ID, + }, + }, []string{}) + } + } +} + +func (b *DefaultTerraformBuider) buildRoutes(content *file.Content, controlPlaneID *string) { + for _, route := range content.Routes { + parentResourceName := strings.ReplaceAll(*route.Name, "-", "_") + parents := map[string]string{} + if route.Service != nil { + parents["service"] = strings.ReplaceAll(*route.Service.Name, "-", "_") + } + b.content += generateResource("gateway_route", parentResourceName, toMapAny(route), parents, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": route.ID, + }, + }, []string{}) + + for _, plugin := range route.Plugins { + resourceName := strings.ReplaceAll(*plugin.Name, "-", "_") + b.content += generateResource("gateway_plugin", resourceName, toMapAny(plugin), map[string]string{ + "route": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": plugin.ID, + }, + }, []string{}) + } + } +} + +func (b *DefaultTerraformBuider) buildGlobalPlugins(content *file.Content, controlPlaneID *string) { + for _, globalPlugin := range content.Plugins { + resourceName := strings.ReplaceAll(*globalPlugin.Name, "-", "_") + b.content += generateResource( + "gateway_plugin", + resourceName, + toMapAny(globalPlugin), + map[string]string{}, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": globalPlugin.ID, + }, + }, + []string{}, + ) + } +} + +func (b *DefaultTerraformBuider) buildConsumers( + content *file.Content, + controlPlaneID *string, + ignoreCredentialChanges bool, +) { + for _, consumer := range content.Consumers { + parentResourceName := strings.ReplaceAll(*consumer.Username, "-", "_") + b.content += generateResource( + "gateway_consumer", + parentResourceName, + toMapAny(consumer), + map[string]string{}, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": consumer.ID, + }, + }, + []string{}, + ) + + for _, cg := range consumer.Groups { + resourceName := strings.ReplaceAll(*cg.Name, "-", "_") + + b.content += generateRelationship( + "gateway_consumer_group_member", + resourceName+"_"+parentResourceName, + map[string]string{ + "consumer": parentResourceName, + "consumer_group": resourceName, + }, + toMapAny(consumer), + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "consumer_id": consumer.ID, + "consumer_group_id": cg.ID, + }, + }, + ) + } + + for _, acl := range consumer.ACLGroups { + resourceName := "acl_" + strings.ReplaceAll(*acl.Group, "-", "_") + b.content += generateResource("gateway_acl", resourceName, toMapAny(acl), map[string]string{ + "consumer_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": acl.ID, + "consumer_id": consumer.ID, + }, + }, []string{}) + } + + for _, basicauth := range consumer.BasicAuths { + lifecycle := []string{} + + if ignoreCredentialChanges { + lifecycle = []string{ + "password", + } + } + + resourceName := "basic_auth_" + strings.ReplaceAll(*basicauth.Username, "-", "_") + b.content += generateResource("gateway_basic_auth", resourceName, toMapAny(basicauth), map[string]string{ + "consumer_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": basicauth.ID, + "consumer_id": consumer.ID, + }, + }, lifecycle) + } + + for _, keyauth := range consumer.KeyAuths { + resourceName := "key_auth_" + strings.ReplaceAll(*keyauth.Key, "-", "_") + b.content += generateResource("gateway_key_auth", resourceName, toMapAny(keyauth), map[string]string{ + "consumer_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": keyauth.ID, + "consumer_id": consumer.ID, + }, + }, []string{}) + } + + for _, jwt := range consumer.JWTAuths { + lifecycle := []string{} + + if ignoreCredentialChanges { + lifecycle = []string{ + "secret", "key", + } + } + resourceName := "jwt_" + strings.ReplaceAll(*jwt.Key, "-", "_") + b.content += generateResource("gateway_jwt", resourceName, toMapAny(jwt), map[string]string{ + "consumer_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": jwt.ID, + "consumer_id": consumer.ID, + }, + }, lifecycle) + } + + for _, hmacauth := range consumer.HMACAuths { + resourceName := "hmac_auth_" + strings.ReplaceAll(*hmacauth.Username, "-", "_") + b.content += generateResource("gateway_hmac_auth", resourceName, toMapAny(hmacauth), map[string]string{ + "consumer_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": hmacauth.ID, + "consumer_id": consumer.ID, + }, + }, []string{}) + } + + for _, plugin := range consumer.Plugins { + pluginName := strings.ReplaceAll(*plugin.Name, "-", "_") + b.content += generateResource("gateway_plugin", pluginName, toMapAny(plugin), map[string]string{ + "consumer": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": plugin.ID, + }, + }, []string{}) + } + + } +} + +func (b *DefaultTerraformBuider) buildConsumerGroups(content *file.Content, controlPlaneID *string) { + for _, cg := range content.ConsumerGroups { + parentResourceName := strings.ReplaceAll(*cg.Name, "-", "_") + parents := map[string]string{} + b.content += generateResource("gateway_consumer_group", parentResourceName, toMapAny(cg), parents, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": cg.ID, + }, + }, []string{}) + + // We intentionally don't generate consumers here. Consumers is a FK reference, not a definition. + for _, consumer := range cg.Consumers { + resourceName := strings.ReplaceAll(*consumer.Username, "-", "_") + + b.content += generateRelationship( + "gateway_consumer_group_member", + parentResourceName+"_"+resourceName, + map[string]string{ + "consumer": resourceName, + "consumer_group": parentResourceName, + }, + toMapAny(consumer), + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "consumer_id": consumer.ID, + "consumer_group_id": cg.ID, + }, + }, + ) + } + + for _, plugin := range cg.Plugins { + resourceName := strings.ReplaceAll(*plugin.Name, "-", "_") + b.content += generateResource("gateway_plugin", resourceName, toMapAny(plugin), map[string]string{ + "consumer_group": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": plugin.ID, + }, + }, []string{}) + } + } +} + +func (b *DefaultTerraformBuider) buildUpstreams(content *file.Content, controlPlaneID *string) { + for _, upstream := range content.Upstreams { + parentResourceName := strings.ReplaceAll(*upstream.Name, "-", "_") + parentResourceName = "upstream_" + strings.ReplaceAll(parentResourceName, ".", "_") + parents := map[string]string{} + b.content += generateResource("gateway_upstream", parentResourceName, toMapAny(upstream), parents, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": upstream.ID, + }, + }, []string{}) + + for _, target := range upstream.Targets { + resourceName := strings.ReplaceAll(*target.Target.Target, ".", "_") + resourceName = "target_" + strings.ReplaceAll(resourceName, ":", "_") + b.content += generateResource("gateway_target", resourceName, toMapAny(target), map[string]string{ + "upstream_id": parentResourceName, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": target.ID, + "upstream_id": upstream.ID, + }, + }, []string{}) + } + } +} + +func (b *DefaultTerraformBuider) buildCACertificates(content *file.Content, controlPlaneID *string) { + idx := 0 + for _, caCertificate := range content.CACertificates { + hashedCert := fmt.Sprintf("%x", md5.Sum([]byte(*caCertificate.Cert))) //nolint:gosec + resourceName := "ca_cert_" + hashedCert + idx++ + b.content += generateResource( + "gateway_ca_certificate", + resourceName, + toMapAny(caCertificate), + map[string]string{}, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": caCertificate.ID, + }, + }, + []string{}, + ) + } +} + +func (b *DefaultTerraformBuider) buildCertificates(content *file.Content, controlPlaneID *string) { + for _, certificate := range content.Certificates { + hashedCert := fmt.Sprintf("%x", md5.Sum([]byte(*certificate.Cert))) //nolint:gosec + resourceName := "cert_" + hashedCert + b.content += generateResource( + "gateway_certificate", + resourceName, + toMapAny(certificate), + map[string]string{}, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": certificate.ID, + }, + }, + []string{}, + ) + + for _, sni := range certificate.SNIs { + resourceName := "sni_" + strings.ReplaceAll(*sni.Name, ".", "_") + b.content += generateResource("gateway_sni", resourceName, toMapAny(sni), map[string]string{ + "certificate": "cert_" + hashedCert, + }, importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": sni.ID, + }, + }, []string{}) + } + } +} + +func (b *DefaultTerraformBuider) buildVaults(content *file.Content, controlPlaneID *string) { + for _, vault := range content.Vaults { + parentResourceName := strings.ReplaceAll(*vault.Name, "-", "_") + parents := map[string]string{} + b.content += generateResourceWithCustomizations( + "gateway_vault", + parentResourceName, + toMapAny(vault), + parents, + map[string]string{ + "config": "jsonencode", + }, + importConfig{ + controlPlaneID: controlPlaneID, + importValues: map[string]*string{ + "id": vault.ID, + }, + }, + []string{}, + ) + } +} + +func (b *DefaultTerraformBuider) getContent() string { + return b.content +} diff --git a/kong2tf/generate_resource.go b/kong2tf/generate_resource.go new file mode 100644 index 000000000..07295ee57 --- /dev/null +++ b/kong2tf/generate_resource.go @@ -0,0 +1,393 @@ +package kong2tf + +import ( + "fmt" + "sort" + "strings" +) + +type importConfig struct { + controlPlaneID *string + importValues map[string]*string +} + +func generateResource( + entityType, + name string, + entity map[string]any, + parents map[string]string, + imports importConfig, + lifecycle []string, +) string { + return generateResourceWithCustomizations(entityType, name, entity, parents, map[string]string{}, imports, lifecycle) +} + +func generateResourceWithCustomizations( + entityType, + name string, + entity map[string]any, + parents map[string]string, + customizations map[string]string, + imports importConfig, + lifecycle []string, +) string { + // Cache ID in case we need to use it for imports + entityID := "" + if entity["id"] != nil { + entityID = entity["id"].(string) + } + + // Populate parents with foreign keys as needed + parentKeys := []string{"service", "route", "consumer", "upstream", "certificate", "consumer_group"} + + // Populate parents with foreign keys as needed + for _, key := range parentKeys { + if entity[key] != nil { + // Switch on type of parent + switch entity[key].(type) { + case string: + parents[key] = entity[key].(string) + case map[string]interface{}: + parents[key] = entity[key].(map[string]interface{})["name"].(string) + default: + panic(fmt.Sprintf("Unknown type for parent %s", key)) + } + } + } + + // List of keys to remove + removeKeys := []string{ + "id", + } + + // Build a map of entity types to keys + entityTypeToKeys := map[string][]string{ + "gateway_service": {"routes", "plugins"}, + "gateway_route": {"plugins", "service"}, + "gateway_plugin": {"service", "route", "consumer"}, + "gateway_consumer": { + "groups", "acls", "basicauth_credentials", "keyauth_credentials", + "jwt_secrets", "hmacauth_credentials", "basicauth_credentials", "plugins", + }, + "gateway_upstream": {"targets"}, + "gateway_consumer_group": {"consumers", "plugins"}, + "gateway_certificate": {"snis"}, + } + + if additionalKeys := entityTypeToKeys[entityType]; additionalKeys != nil { + removeKeys = append(removeKeys, additionalKeys...) + } + + // Remove keys that are not needed + for _, k := range removeKeys { + delete(entity, k) + } + + if entityType == "gateway_plugin" { + entityType = fmt.Sprintf("%s_%s", entityType, name) + delete(entity, "name") + } + + // We don't need to prefix SNIs with the Cert name + // Or routes with the service name + if entityType != "gateway_sni" && entityType != "gateway_route" { + for k := range parents { + name = fmt.Sprintf("%s_%s", strings.ReplaceAll(parents[k], "-", "_"), name) + } + } + + s := fmt.Sprintf(` +resource "konnect_%s" "%s" { +%s + +%s control_plane_id = var.control_plane_id%s +} +`, + entityType, name, + strings.TrimRight(output(entityType, entity, 1, true, "\n", customizations), "\n"), + generateParents(parents), + generateLifecycle(lifecycle)) + + // Generate imports + if imports.controlPlaneID != nil && entityID != "" { + entity["id"] = entityID + s += generateImports(entityType, name, imports.importValues, imports.controlPlaneID) + } + + return strings.TrimSpace(s) + "\n\n" +} + +func generateRelationship( + entityType string, + name string, + relations map[string]string, + _ map[string]any, // 'entity' when TODO is resolved + _ importConfig, // 'imports' when TODO is resolved +) string { + // TODO: We don't support relationship importing in the provider yet + // entityID := entity["id"].(string) + + s := fmt.Sprintf(`resource "konnect_%s" "%s" {`, entityType, name) + + // Extract keys to iterate in a deterministic order + keys := make([]string, 0) + for k := range relations { + keys = append(keys, k) + } + + sort.Strings(keys) + + // Output each item in the relationship + for _, k := range keys { + s += fmt.Sprintf("\n"+` %s_id = konnect_gateway_%s.%s.id`, k, k, relations[k]) + } + s += "\n control_plane_id = var.control_plane_id" + s += "\n}\n\n" + + // TODO: We don't support relationship importing in the provider yet + /* + │ Error: Not Implemented + │ + │ No available import state operation is available for resource gateway_consumer_group_member. + */ + //if imports.controlPlaneID != nil { + // entity["id"] = entityID + // s += generateImports(entityType, name, entity, imports.importValues, imports.controlPlaneID) + "\n\n" + //} + + return s +} + +func generateImports( + entityType string, + name string, + keysFromEntity map[string]*string, + cpID *string, +) string { + if len(keysFromEntity) == 0 { + return "" + } + + return fmt.Sprintf("\n"+`import { + to = konnect_%s.%s + id = "%s" +}`, entityType, name, generateImportKeys(keysFromEntity, cpID)) +} + +func generateImportKeys(keys map[string]*string, cpID *string) string { + if len(keys) == 0 { + return "" + } + + s := "{" + for k, val := range keys { + s += fmt.Sprintf(`\"%s\": \"%s\", `, k, *val) + } + + s += fmt.Sprintf(`\"control_plane_id\": \"%s\", `, *cpID) + + s = strings.TrimRight(s, ", ") + + s += "}" + + return s +} + +func generateLifecycle(lifecycle []string) string { + if len(lifecycle) == 0 { + return "" + } + + s := ` + lifecycle { + ignore_changes = [` + for _, l := range lifecycle { + s += "\n " + l + "," + } + s = strings.TrimRight(s, ",") + + s += ` + ] + } +` + + return s +} + +func generateParents(parents map[string]string) string { + if len(parents) == 0 { + return "" + } + + var result []string + for k, v := range parents { + v = strings.ReplaceAll(v, "-", "_") + // if parent ends with _id, use it as-is + if strings.HasSuffix(k, "_id") { + result = append(result, fmt.Sprintf(` %s = konnect_gateway_%s.%s.id`, k, strings.TrimSuffix(k, "_id"), v)+"\n") + continue + } + result = append(result, fmt.Sprintf(` %s = { + id = konnect_gateway_%s.%s.id + }`+"\n", k, k, v)) + } + + return strings.Join(result, "\n") + "\n" +} + +// Output function that handles the dynamic data +func output( + entityType string, + object map[string]interface{}, + depth int, + isRoot bool, + eol string, + customizations map[string]string, +) string { + var result []string + + // Loop through object in order of keys + keys := make([]string, 0) + for k := range object { + keys = append(keys, k) + } + + sort.Strings(keys) + + // Move the most common keys to the front + var prioritizedKeys []string + for _, k := range []string{"enabled", "name", "username"} { + if _, exists := object[k]; exists { + prioritizedKeys = append(prioritizedKeys, k) + } + } + + // Append the rest of the keys + for _, k := range keys { + if contains(prioritizedKeys, k) { + continue + } + if k != "name" && k != "enabled" { + prioritizedKeys = append(prioritizedKeys, k) + } + } + keys = prioritizedKeys + + for _, k := range keys { + v := object[k] + + // TODO: Remove this once deck dump doesn't export nil values + if v == nil { + continue + } + + switch v := v.(type) { + case map[string]interface{}: + result = append(result, outputHash(entityType, k, v, depth, isRoot, eol, customizations)) + case []interface{}: + result = append(result, outputList(entityType, k, v, depth)) + default: + result = append(result, line(fmt.Sprintf("%s = %s", k, quote(v)), depth, eol)) + } + } + return strings.Join(result, "") +} + +// Handles rendering a map (hash) in Go +func outputHash( + entityType string, + key string, + input map[string]interface{}, + depth int, + isRoot bool, + eol string, + customizations map[string]string, +) string { + s := "" + if !isRoot { + s += "\n" + } + + custom := customizations[key] + + if custom != "" { + s += line(fmt.Sprintf("%s = %s({", key, custom), depth, eol) + } else { + s += line(fmt.Sprintf("%s = {", key), depth, eol) + } + + s += output(entityType, input, depth+1, true, eol, customizations) + + if custom != "" { + s += line("})", depth, eol) + } else { + s += line("}", depth, eol) + } + return s +} + +// Handles rendering a map within a list in Go +func outputHashInList(entityType string, input map[string]interface{}, depth int) string { + s := "\n" + s += line("{", depth+1, "\n") + s += output(entityType, input, depth+2, false, "\n", map[string]string{}) + s += line("},", depth+1, "\n") + return s +} + +// Handles rendering a list (array) in Go +func outputList(entityType string, key string, input []interface{}, depth int) string { + s := line(fmt.Sprintf("%s = [", key), depth, "") + for _, v := range input { + switch v := v.(type) { + case map[string]interface{}: + s += outputHashInList(entityType, v, depth) + default: + s += fmt.Sprintf("%s, ", quote(v)) + } + } + s = strings.TrimRight(s, ", ") + s += endList(input, depth) + return s +} + +// Ends a list rendering in Go +func endList(input []interface{}, depth int) string { + lastLine := line("]", depth, "\n") + if _, ok := input[len(input)-1].(map[string]interface{}); ok { + return lastLine + } + return strings.TrimLeft(lastLine, " ") +} + +// Formats a line with proper indentation and end-of-line characters +func line(input string, depth int, eol string) string { + return strings.Repeat(" ", depth) + input + eol +} + +// Properly quotes a value based on its type +func quote(input interface{}) string { + switch v := input.(type) { + case nil: + return "" + case bool, int, float64: + return fmt.Sprintf("%v", v) + case string: + if strings.Contains(v, "\n") { + return fmt.Sprintf("<" + client_secret: + - "" + session_secret: "" + response_mode: form_post diff --git a/kong2tf/testdata/global-plugin-oidc-output-expected.tf b/kong2tf/testdata/global-plugin-oidc-output-expected.tf new file mode 100644 index 000000000..0b402b1f4 --- /dev/null +++ b/kong2tf/testdata/global-plugin-oidc-output-expected.tf @@ -0,0 +1,19 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_plugin_openid_connect" "openid_connect" { + enabled = true + config = { + auth_methods = ["authorization_code", "session"] + client_id = [""] + client_secret = [""] + issuer = "http://example.org" + response_mode = "form_post" + session_secret = "" + } + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/global-plugin-rate-limiting-input.yaml b/kong2tf/testdata/global-plugin-rate-limiting-input.yaml new file mode 100644 index 000000000..121a06a1e --- /dev/null +++ b/kong2tf/testdata/global-plugin-rate-limiting-input.yaml @@ -0,0 +1,7 @@ +plugins: + - name: rate-limiting + enabled: false + config: + second: 5 + hour: 10000 + policy: local diff --git a/kong2tf/testdata/global-plugin-rate-limiting-output-expected.tf b/kong2tf/testdata/global-plugin-rate-limiting-output-expected.tf new file mode 100644 index 000000000..5a19a1acb --- /dev/null +++ b/kong2tf/testdata/global-plugin-rate-limiting-output-expected.tf @@ -0,0 +1,16 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_plugin_rate_limiting" "rate_limiting" { + enabled = false + config = { + hour = 10000 + policy = "local" + second = 5 + } + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/input.yaml b/kong2tf/testdata/input.yaml new file mode 100644 index 000000000..4e5421e74 --- /dev/null +++ b/kong2tf/testdata/input.yaml @@ -0,0 +1,517 @@ +vaults: +- config: + prefix: MY_SECRET_ + description: ENV vault for secrets + name: env + prefix: my-env-vault + tags: + - env-vault +ca_certificates: +- cert: | + -----BEGIN CERTIFICATE----- + MIIBfDCCASKgAwIBAgIRAJqcZC1VSvSQLMN1+7yAeswwCgYIKoZIzj0EAwIwHDEa + MBgGA1UEAxMRRGVtbyBLb25nIFJvb3QgQ0EwHhcNMjIwNjEzMTMzNzMzWhcNMjcw + NjEzMTkzNzMzWjAcMRowGAYDVQQDExFEZW1vIEtvbmcgUm9vdCBDQTBZMBMGByqG + SM49AgEGCCqGSM49AwEHA0IABOGR89IyhreSHRAi6wp9a5DBIDp4YYSdWzuEdlNx + 7pX1G4T7x68xUXJZXRUPFyT8Xzn5KwCJm8RVT+nAhrsUx6SjRTBDMA4GA1UdDwEB + /wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1UdDgQWBBQ9CUiOPhjp7KD2 + ScRDxX4IzDOrNzAKBggqhkjOPQQDAgNIADBFAiEAw6Dov0c0L++1W9VufAfSMdNR + PSDfPU0MiUiG59/VIBICIEFn/6c5eQc3hUUBL74/RmNT2b1zxBmp7RiPXJAnAAwJ + -----END CERTIFICATE----- + cert_digest: f1baffe9fe9cf8497e38a4271d67fab44423678b7e7c0f677a50f37c113d81b5 + id: 8ccca140-aff4-411d-8e1c-fcfb3f932a8e + tags: + - root-ca +- cert: | + -----BEGIN CERTIFICATE----- + MIIBqTCCAVCgAwIBAgIQb5LqGa9gS3+Mc2ntWfSoJjAKBggqhkjOPQQDAjAcMRow + GAYDVQQDExFEZW1vIEtvbmcgUm9vdCBDQTAeFw0yMjA2MTMxMzM5MTVaFw0yMzA2 + MTMxOTM5MTVaMCoxKDAmBgNVBAMTH0RlbW8gS29uZyBSb290IEludGVybWVkaWF0 + ZTEgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQImvnSuvXkGy88lvx8a7of + e0MEMRI2siVvybvWXNpeXXlixgaq7weJ7pewf3HywfO68Va6kn8ehWh7s0D7SLHM + o2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4E + FgQUhuxCKmCSvZWf95+iZ+Wsz9DJJVMwHwYDVR0jBBgwFoAUPQlIjj4Y6eyg9knE + Q8V+CMwzqzcwCgYIKoZIzj0EAwIDRwAwRAIgNZ+JPA1OqF5DsPapAZ2YsUOgIpn3 + ZbQuYKCAV0SD4EcCIFnfA5rWrc1AgtUw5inJQqJQRNgoPuC14vACqI48BiRl + -----END CERTIFICATE----- + cert_digest: dbef7ed285fb292e24f84ffba93c48d92fa322387d85469c460c655abedd5308 + id: 2e5f8ad1-21ca-47f5-8af7-899486e82731 + tags: + - intermediate_ca1 +- cert: | + -----BEGIN CERTIFICATE----- + MIIBujCCAV+gAwIBAgIRAMkGpj7WZf+2RFE/q7ZhejEwCgYIKoZIzj0EAwIwKjEo + MCYGA1UEAxMfRGVtbyBLb25nIFJvb3QgSW50ZXJtZWRpYXRlMSBDQTAeFw0yMjA2 + MTMxMzQwNTFaFw0yMjEyMTMwNDQwNTFaMCoxKDAmBgNVBAMTH0RlbW8gS29uZyBS + b290IEludGVybWVkaWF0ZTIgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQB + my/zhZ3F2HvHFqtQzuD3lXX8SeYakxiBQvaGkGSLKD67N3vh7iC2rTSdj/vAs8ws + Y9X+mXzS6GDKC8PbSX6xo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgw + BgEB/wIBADAdBgNVHQ4EFgQURwCm53YPStZoAMfnVyknH4IgZa4wHwYDVR0jBBgw + FoAUhuxCKmCSvZWf95+iZ+Wsz9DJJVMwCgYIKoZIzj0EAwIDSQAwRgIhAN1pkUKc + azM4PiXOnkILB2KBDIF4XpHf+4ThDMODzXP8AiEA45KXA3qMrRPQV1oBfWZ3hLgX + gxUhveuHBXMWnzUbn6U= + -----END CERTIFICATE----- + cert_digest: 45b2b6dd9d4102955b1b1e4b540e677f140521462ed4f22fa5a713863ca84600 + id: cab2565d-fed6-4a07-b25c-bab76fdfe071 + tags: + - intermediate_ca2 +certificates: +- cert: |- + -----BEGIN CERTIFICATE----- + MIIECTCCAvGgAwIBAgIUAusYGP9BwoLFFAJdB/jY6eUzUyAwDQYJKoZIhvcNAQEL + BQAwgZIxCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlIYW1wc2hpcmUxEjAQBgNVBAcM + CUFsZGVyc2hvdDEQMA4GA1UECgwHS29uZyBVSzEQMA4GA1UECwwHU3VwcG9ydDEY + MBYGA1UEAwwPU3VwcG9ydCBSb290IENBMR0wGwYJKoZIhvcNAQkBFg5zdHVAa29u + Z2hxLmNvbTAeFw0yMTAxMTUxMTE5NDNaFw0yMjA1MzAxMTE5NDNaMIGRMQswCQYD + VQQGEwJVSzESMBAGA1UECAwJSGFtcHNoaXJlMRIwEAYDVQQHDAlBbGRlcnNob3Qx + EDAOBgNVBAoMB0tvbmcgVUsxEDAOBgNVBAsMB1N1cHBvcnQxFzAVBgNVBAMMDnBy + b3h5LmtvbmcubGFuMR0wGwYJKoZIhvcNAQkBFg5zdHVAa29uZ2hxLmNvbTCCASIw + DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJUbKiqoCK1BTNk/7l42n6ukyTEu + eLyB23e/90PzT/oz8wZzgwTodzbFAS2VtFr1EKqFzor0DrXp9CLnebOdiAR3I6LD + /WY/x0KW3lx3F35fGiUOSLPTH8zeiDTMx11CcKDxesA+M2/s5q0igkOQ4z4w3voz + m5a52IcQTSA8K5knNU1qUZBLpc+khxFcaheEK1jsISJJhcdizZBfGdk8S1vpyj5F + uCZ7oaRvNA6imHjSJwpgo36zd84TgrIgVj9R4QtJysWy/X+bbaKUiKBWwAtd4+DT + EP90l/ny9szu2fijk4/6k1ntXufGTyvM+J0/qJ13e99TVYOVanITnpTO+6cCAwEA + AaNWMFQwHwYDVR0jBBgwFoAUdskpf0wJRQxjtzQFZciWmUfl2bcwCQYDVR0TBAIw + ADALBgNVHQ8EBAMCBPAwGQYDVR0RBBIwEIIOcHJveHkua29uZy5sYW4wDQYJKoZI + hvcNAQELBQADggEBAJVrTWQRQzNtypa9OXFYADm8Fay1VMop3BY2kh0tfYgQEJ/4 + pJUj6CaszQZ/Aix6LaPnXFcoPCDqqv00mgju86PMamr/zA9USXk8eTmzJkp5RklS + GdqiXboqESiQVvaNz3kdW7wgNz4FwaGCzkEi/dcc2LdtzLpWizx+TlxMMqjonUUM + ovZgZo+OlhWRsDVT/qy5SFtA0vlVNtdBr2egXb1H7J8UDC+fax/iKa7+fBUHZOO9 + Fk9U8bxgfQ+jPIVVL8CfAtR68Sos7NpWH0S2emqZRnQvf0MSNdkTQKWn4qR9sckj + Ewxs5FbrMmgCOgwk1PtgRmdP3RME0HwK/B03saQ= + -----END CERTIFICATE----- + id: 507cc555-5b92-496d-9e89-bfc78dfcddbe + key: |- + -----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCVGyoqqAitQUzZ + P+5eNp+rpMkxLni8gdt3v/dD80/6M/MGc4ME6Hc2xQEtlbRa9RCqhc6K9A616fQi + 53mznYgEdyOiw/1mP8dClt5cdxd+XxolDkiz0x/M3og0zMddQnCg8XrAPjNv7Oat + IoJDkOM+MN76M5uWudiHEE0gPCuZJzVNalGQS6XPpIcRXGoXhCtY7CEiSYXHYs2Q + XxnZPEtb6co+Rbgme6GkbzQOoph40icKYKN+s3fOE4KyIFY/UeELScrFsv1/m22i + lIigVsALXePg0xD/dJf58vbM7tn4o5OP+pNZ7V7nxk8rzPidP6idd3vfU1WDlWpy + E56UzvunAgMBAAECggEAcq7lHNAHdHLgT8yrY41x/AwPryNGO/9JNW7SGVKAdjoU + tyaLZHAEmXynLk+R28/YFMA8H4Yd9m9OlrYhVjRZFM4p+5vxP/7YHPB7cPUsfcda + DZxR8PX25JVYf/vtb16V0ERLnKd62qCEwC/lr2A2WHQwXJLrHeAtmZzBJYUCJ5Xj + Dv1ZhyFjknswaV0vGXe6njTI6CzMQDTGysoagpCCo7RWgzjLREg2BGWd2UQpY4AW + nYAP4QNt82UMQbgIqsEMj64mzS9Q+o1P84J1naSP5sCI22LeFRp6iezZc+D8EH/5 + RNONGSNWl3g6bsvN1VywYwjWn+waD3XAjXUu+peUgQKBgQDDu1QP28oSMKivHdfo + kQ1HrTNBRc9eWeAMZFuIbbPLN8rdEibpOm3DwTqithnahqS0NLOsBnXNtuLw1Qr/ + zmOzn0yDO5XG8dlKr9vqWeBLdcRydLJBZwqEzWf4JwwwgfK3rItRaIbnAxnGUTS5 + SrrhNfBAIGS9jx5X2kvLC7hFQQKBgQDDBIrpLTIjRDloWZcu03z9Bhb8jQCyGb5C + 4MYs+duCnQIdNq/+maPspJzbVmF4b6S1zIPweI3fMvMeqRTbuf+drpElst1buFTO + P0UMMn4V+4qGIOOkIy5JIKwR8sJD9tNDUPtxuDEotTB9IyWx6pdmCFz5v/bggDCu + reoqflL+5wKBgQCDvb+L2QS+j4/KJk0flRoaJ2K7SVCVEesyjA3r2uLMImZhvAkC + rDgbLSDZSbZHFp8fR+WzILoD11gSf2Ki4PjMeqkWH3HlcP0vPwTHTO0h/UdXPmKI + kOFMl7CmHyoeMCj9JZ60EaXTMYwUpq3VFY6JbTOjBeqoh/8FZMHlDaNewQKBgCHg + ECEg8KyflTlDFrfTlMp+3E9STuShBCOp18LIRBEUJOHeNgQLvCXHElgnURcSjZHm + zKRgzIQQ3Zpd1Hm2fWhuglgCEeF0y4ZoBx5vRueaoh1aaTCBy/B39GvJt2UG4vu2 + fXbrf96KWrnh+RJGpbXbjgr0BXZJzisJmrt25gPRAoGBAI3c+INpQXwrE+LBzCPu + LwIVvkm5NpeIlKQtDNrqG1QvUhqyZ2/Xitc4FyiccW7WHxkGKGZyj7GbmpqEOnyY + iVku0LSftZgycet2uMdp0HaVAgi5S6aVf5yN0U/8R5ToxcbuEfqwrBIyRgse8lx3 + NNSvLxPAempmiFPSk9AtobYV + -----END PRIVATE KEY----- + snis: + - name: proxy.kong.lan + tags: + - proxy.kong.lan +- cert: |- + -----BEGIN CERTIFICATE----- + MIIFeDCCBGCgAwIBAgIUAusYGP9BwoLFFAJdB/jY6eUzUyQwDQYJKoZIhvcNAQEL + BQAwgZIxCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlIYW1wc2hpcmUxEjAQBgNVBAcM + CUFsZGVyc2hvdDEQMA4GA1UECgwHS29uZyBVSzEQMA4GA1UECwwHU3VwcG9ydDEY + MBYGA1UEAwwPU3VwcG9ydCBSb290IENBMR0wGwYJKoZIhvcNAQkBFg5zdHVAa29u + Z2hxLmNvbTAeFw0yMTAxMjAxNTA0NDVaFw0yMjAxMjAxNTA0NDVaMIGQMQswCQYD + VQQGEwJVSzESMBAGA1UECAwJSGFtcHNoaXJlMRIwEAYDVQQHDAlBbGRlcnNob3Qx + EDAOBgNVBAoMB0tvbmcgVUsxEDAOBgNVBAsMB1N1cHBvcnQxFjAUBgNVBAMMDW10 + bHMtY29uc3VtZXIxHTAbBgkqhkiG9w0BCQEWDnN0dUBrb25naHEuY29tMIICIjAN + BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1/+83/YNiEVKYvcuVwYGve6afsg1 + BYCn1+E9Uwgh0uwAenT/DKB8NhqoVxc7cZ2HaTI146IGmFICmctlTWvLPLglHmTo + byOUV6tIJAjvzyEOpC458hLGgbv8mhGXJWPxBVu7Wy6Hapz2bk0cEscfL7PHKaRu + 3D6r8/zbhhWAqe4EIt+NVYT6baaYBs7bPZQXs/sluKI+DNYuDeaAmoSuCc4ein6z + 0xDqCSMmPebzjns03ttB29vWL3eYY9dvgoCd+CPhXT/C4CHtvKbH+hOQYDtVF6MO + 1mmABAQTQWMR/00+QI0xtvuXtEPurla5dA0TN6ddCTOOcILKx62z5oc3Kqr+nHHa + 71zNzARUVaZ2vy1pRVr0DZgB7KqcFXhy/oy8IpmxUR1ASBDZl6B6RKrdQwvgLgmn + 3M/roNLAU+3nz4itpt/zf+X0suwdthrflic1R68z1SlYbyoGARWkZ/pOl6kLNVK2 + OsqQuICaajnW7t1oDd7z1+3hm+uoryDwvG6f3T9ZvWjKXYcKg7b+BjbFdahbDywD + PgnhSz9AaoVWhR+GHIPrjRClMpEkra/yGJFvH3UpXhgg9d0DrLZE51Z75a9SvnAj + vdLuNhx4bJbwLBgNGsJMkupzBrw4iCfbKFcBbP8o0Xjtarj7T/mkWuQ1GjWqfyrD + 55NecBPNw5C9BR0CAwEAAaOBxTCBwjAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQE + AwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgQ2xpZW50IENl + cnRpZmljYXRlMB0GA1UdDgQWBBSV3F+eicU8SVT4LcDJ6eMzP0todzAfBgNVHSME + GDAWgBR2ySl/TAlFDGO3NAVlyJaZR+XZtzAOBgNVHQ8BAf8EBAMCBeAwHQYDVR0l + BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4IBAQB5L0OZ + WELG9Pw6Ol1BsZYgpLR4PGNBB9dKm/9dd+q+ohZVFCfXcjZ3YOU1vh/HHQrALRNY + I58JxcVCOx/qIW2uA0iSCqIT0sNb9cJLxfZf7X+BzPPPnu0ugUJp7GzLNnHitrLC + Xb1nmmefwgraNzp+a5IrR8RcQG1mYDuS+2HK/rybo22XcCxhob8OiDEn8+ytkKyQ + Ipmrf9D+/68/ih6az0w1aakASMmFe8z/p6VgVQkCySCWWFG525BRdGmSImqVZ4xa + aQFN3L+oN+JJcCFTthLOAYo32JH+xLMz7PokzSL84g3b68h59hXDoMSwB10GthL5 + T8tqV6i5miKWwvfZ + -----END CERTIFICATE----- + id: f3ae1bb2-ea6a-4caf-a7a7-2f078b7842db + key: |- + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- +plugins: +- name: openid-connect + config: + auth_methods: + - authorization_code + - session + issuer: http://example.org + client_id: + - "" + client_secret: + - "" + session_secret: "" + response_mode: form_post +services: + - name: example-service + url: http://example-api.com + protocol: http + host: example-api.com + port: 80 + path: /v1 + retries: 5 + connect_timeout: 5000 + write_timeout: 60000 + read_timeout: 60000 + enabled: true + client_certificate: 4e3ad2e4-0bc4-4638-8e34-c84a417ba39b + plugins: + - name: rate-limiting-advanced + config: + limit: + - 5 + window_size: + - 30 + identifier: consumer + sync_rate: -1 + namespace: example_namespace + strategy: local + hide_client_headers: false + ordering: + before: + access: + - another-plugin + after: + access: + - yet-another-plugin + tags: + - example + - api + routes: + - name: example-route + methods: + - GET + - POST + hosts: + - example.com + - another-example.com + - yet-another-example.com + paths: + - ~/v1/example/?$ + - /v1/another-example + - /v1/yet-another-example + protocols: + - http + - https + headers: + x-my-header: + - ~*foos?bar$ + x-another-header: + - first-header-value + - second-header-value + regex_priority: 1 + strip_path: false + preserve_host: true + tags: + - version:v1 + https_redirect_status_code: 302 + snis: + - example.com + plugins: + - name: aws-lambda + config: + aws_key: my_key + aws_secret: my_secret + function_name: my_function + aws_region: us-west-2 + - name: cors + config: + origins: + - example.com + methods: + - GET + - POST + headers: + - Authorization + exposed_headers: + - X-My-Header + max_age: 3600 + credentials: true + - name: file-log + config: + path: /var/log/kong/kong.log + reopen: true + - name: http-log + config: + http_endpoint: http://example.com/logs + method: POST + content_type: application/json + timeout: 10000 + keepalive: 60000 + retry_count: 10 + queue_size: 1000 + - name: ip-restriction + config: + allow: + - 192.168.0.1/24 + deny: + - 192.168.0.2/32 + - name: rate-limiting-advanced + config: + limit: + - 5 + window_size: + - 30 + identifier: consumer + sync_rate: -1 + namespace: example_namespace + strategy: local + hide_client_headers: false + - name: request-termination + config: + status_code: 403 + message: Forbidden + - name: response-ratelimiting + config: + limits: + limit_name: + minute: 10 + policy: local + - name: tcp-log + config: + host: example.com + port: 1234 + - name: basic-auth + config: + hide_credentials: false + - name: jwt + config: + uri_param_names: + - token + claims_to_verify: + - exp + - nbf + key_claim_name: kid + secret_is_base64: false + anonymous: null + run_on_preflight: true + maximum_expiration: 3600 + header_names: + - Authorization + - name: key-auth + config: + hide_credentials: false + key_names: + - apikey + key_in_body: false + run_on_preflight: true + - name: acl + config: + allow: + - admin +upstreams: + - name: example-api.com + algorithm: round-robin + hash_on: none + hash_fallback: none + hash_on_cookie_path: "/" + slots: 10000 + healthchecks: + passive: + type: http + healthy: + http_statuses: + - 200 + - 201 + - 202 + - 203 + - 204 + - 205 + - 206 + - 207 + - 208 + - 226 + - 300 + - 301 + - 302 + - 303 + - 304 + - 305 + - 306 + - 307 + - 308 + successes: 0 + unhealthy: + http_statuses: + - 429 + - 500 + - 503 + timeouts: 0 + http_failures: 0 + tcp_failures: 0 + active: + https_verify_certificate: true + healthy: + http_statuses: + - 200 + - 302 + successes: 0 + interval: 0 + unhealthy: + http_failures: 0 + http_statuses: + - 429 + - 404 + - 500 + - 501 + - 502 + - 503 + - 504 + - 505 + timeouts: 0 + tcp_failures: 0 + interval: 0 + type: http + concurrency: 10 + headers: + x-my-header: + - foo + - bar + x-another-header: + - bla + timeout: 1 + http_path: "/" + https_sni: example.com + threshold: 0 + tags: + - user-level + - low-priority + host_header: example.com + use_srv_name: false + targets: + - target: 10.10.10.10:8000 + weight: 100 + - target: 10.10.10.11:8000 + weight: 100 +consumers: + - username: example-user + custom_id: "1234567890" + tags: + - internal + acls: + - group: acl_group + tags: + - internal + basicauth_credentials: + - username: my_basic_user + password: my_basic_password + tags: + - internal + jwt_secrets: + - key: my_jwt_secret + algorithm: HS256 + secret: my_secret_key + tags: + - internal + keyauth_credentials: + - key: my_api_key + tags: + - internal + mtls_auth_credentials: + - id: cce8c384-721f-4f58-85dd-50834e3e733a + subject_name: example-user@example.com + plugins: + - name: rate-limiting-advanced + config: + limit: + - 5 + window_size: + - 30 + identifier: consumer + sync_rate: -1 + namespace: example_namespace + strategy: local + hide_client_headers: false +consumer_groups: + - name: example-consumer-group + consumers: + - username: example-user + plugins: + - name: rate-limiting-advanced + config: + limit: + - 5 + window_size: + - 30 + identifier: consumer + sync_rate: -1 + namespace: example_namespace + strategy: local + hide_client_headers: false + window_type: sliding + retry_after_jitter_max: 0 + + + \ No newline at end of file diff --git a/kong2tf/testdata/route-input.yaml b/kong2tf/testdata/route-input.yaml new file mode 100644 index 000000000..b4c32430c --- /dev/null +++ b/kong2tf/testdata/route-input.yaml @@ -0,0 +1,48 @@ +services: + - name: example-service + url: http://example-api.com + routes: + - name: example-route + methods: + - GET + - POST + hosts: + - example.com + - another-example.com + - yet-another-example.com + paths: + - ~/v1/example/?$ + - /v1/another-example + - /v1/yet-another-example + protocols: + - http + - https + headers: + x-my-header: + - ~*foos?bar$ + x-another-header: + - first-header-value + - second-header-value + regex_priority: 1 + strip_path: false + preserve_host: true + tags: + - version:v1 + https_redirect_status_code: 302 + snis: + - example.com + sources: + - ip: 192.168.0.1 + destinations: + - ip: 10.10.10.10 + port: 8080 + +routes: + - name: top-level-route + hosts: + - top-level.example.com + - name: top-level-with-service-route + service: + name: example-service + hosts: + - top-level-with-service.example.com diff --git a/kong2tf/testdata/route-output-expected.tf b/kong2tf/testdata/route-output-expected.tf new file mode 100644 index 000000000..bc879c61d --- /dev/null +++ b/kong2tf/testdata/route-output-expected.tf @@ -0,0 +1,67 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_service" "example_service" { + name = "example-service" + host = "example-api.com" + port = 80 + protocol = "http" + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_route" "example_route" { + name = "example-route" + destinations = [ + { + ip = "10.10.10.10" + port = 8080 + }, + ] + headers = { + x-another-header = ["first-header-value", "second-header-value"] + x-my-header = ["~*foos?bar$"] + } + hosts = ["example.com", "another-example.com", "yet-another-example.com"] + https_redirect_status_code = 302 + methods = ["GET", "POST"] + paths = ["~/v1/example/?$", "/v1/another-example", "/v1/yet-another-example"] + preserve_host = true + protocols = ["http", "https"] + regex_priority = 1 + snis = ["example.com"] + sources = [ + { + ip = "192.168.0.1" + }, + ] + strip_path = false + tags = ["version:v1"] + + service = { + id = konnect_gateway_service.example_service.id + } + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_route" "top_level_route" { + name = "top-level-route" + hosts = ["top-level.example.com"] + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_route" "top_level_with_service_route" { + name = "top-level-with-service-route" + hosts = ["top-level-with-service.example.com"] + + service = { + id = konnect_gateway_service.example_service.id + } + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/route-plugin-input.yaml b/kong2tf/testdata/route-plugin-input.yaml new file mode 100644 index 000000000..59d47b0c1 --- /dev/null +++ b/kong2tf/testdata/route-plugin-input.yaml @@ -0,0 +1,21 @@ +services: + - name: example-service + url: http://example-api.com + routes: + - name: example-route + paths: + - ~/v1/example/?$ + plugins: + - name: cors + config: + origins: + - example.com + methods: + - GET + - POST + headers: + - Authorization + exposed_headers: + - X-My-Header + max_age: 3600 + credentials: true \ No newline at end of file diff --git a/kong2tf/testdata/route-plugin-output-expected.tf b/kong2tf/testdata/route-plugin-output-expected.tf new file mode 100644 index 000000000..662d8b4ea --- /dev/null +++ b/kong2tf/testdata/route-plugin-output-expected.tf @@ -0,0 +1,42 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_service" "example_service" { + name = "example-service" + host = "example-api.com" + port = 80 + protocol = "http" + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_route" "example_route" { + name = "example-route" + paths = ["~/v1/example/?$"] + + service = { + id = konnect_gateway_service.example_service.id + } + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_plugin_cors" "example_route_cors" { + config = { + credentials = true + exposed_headers = ["X-My-Header"] + headers = ["Authorization"] + max_age = 3600 + methods = ["GET", "POST"] + origins = ["example.com"] + } + + route = { + id = konnect_gateway_route.example_route.id + } + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/service-input.yaml b/kong2tf/testdata/service-input.yaml new file mode 100644 index 000000000..c25edb3dc --- /dev/null +++ b/kong2tf/testdata/service-input.yaml @@ -0,0 +1,18 @@ +services: + - name: example-service + url: http://example-api.com + protocol: http + host: example-api.com + port: 80 + path: /v1 + retries: 5 + connect_timeout: 5000 + write_timeout: 60000 + read_timeout: 60000 + enabled: true + client_certificate: 4e3ad2e4-0bc4-4638-8e34-c84a417ba39b + tls_verify: true + tls_verify_depth: 1 + tags: + - example + - api \ No newline at end of file diff --git a/kong2tf/testdata/service-output-expected.tf b/kong2tf/testdata/service-output-expected.tf new file mode 100644 index 000000000..59128051e --- /dev/null +++ b/kong2tf/testdata/service-output-expected.tf @@ -0,0 +1,24 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_service" "example_service" { + enabled = true + name = "example-service" + client_certificate = "4e3ad2e4-0bc4-4638-8e34-c84a417ba39b" + connect_timeout = 5000 + host = "example-api.com" + path = "/v1" + port = 80 + protocol = "http" + read_timeout = 60000 + retries = 5 + tags = ["example", "api"] + tls_verify = true + tls_verify_depth = 1 + write_timeout = 60000 + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/service-plugin-input.yaml b/kong2tf/testdata/service-plugin-input.yaml new file mode 100644 index 000000000..b0656dd41 --- /dev/null +++ b/kong2tf/testdata/service-plugin-input.yaml @@ -0,0 +1,22 @@ +services: + - name: example-service + url: http://example-api.com + plugins: + - name: rate-limiting-advanced + config: + limit: + - 5 + window_size: + - 30 + identifier: consumer + sync_rate: -1 + namespace: example_namespace + strategy: local + hide_client_headers: false + ordering: + before: + access: + - another-plugin + after: + access: + - yet-another-plugin \ No newline at end of file diff --git a/kong2tf/testdata/service-plugin-output-expected.tf b/kong2tf/testdata/service-plugin-output-expected.tf new file mode 100644 index 000000000..ea871a4f1 --- /dev/null +++ b/kong2tf/testdata/service-plugin-output-expected.tf @@ -0,0 +1,40 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_service" "example_service" { + name = "example-service" + host = "example-api.com" + port = 80 + protocol = "http" + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_plugin_rate_limiting_advanced" "example_service_rate_limiting_advanced" { + config = { + hide_client_headers = false + identifier = "consumer" + limit = [5] + namespace = "example_namespace" + strategy = "local" + sync_rate = -1 + window_size = [30] + } + ordering = { + after = { + access = ["yet-another-plugin"] + } + before = { + access = ["another-plugin"] + } + } + + service = { + id = konnect_gateway_service.example_service.id + } + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/upstream-target-input.yaml b/kong2tf/testdata/upstream-target-input.yaml new file mode 100644 index 000000000..285ca28f8 --- /dev/null +++ b/kong2tf/testdata/upstream-target-input.yaml @@ -0,0 +1,84 @@ +upstreams: + - name: example-api.com + algorithm: round-robin + hash_on: none + hash_fallback: none + hash_on_cookie_path: "/" + slots: 10000 + healthchecks: + passive: + type: http + healthy: + http_statuses: + - 200 + - 201 + - 202 + - 203 + - 204 + - 205 + - 206 + - 207 + - 208 + - 226 + - 300 + - 301 + - 302 + - 303 + - 304 + - 305 + - 306 + - 307 + - 308 + successes: 0 + unhealthy: + http_statuses: + - 429 + - 500 + - 503 + timeouts: 0 + http_failures: 0 + tcp_failures: 0 + active: + https_verify_certificate: true + healthy: + http_statuses: + - 200 + - 302 + successes: 0 + interval: 0 + unhealthy: + http_failures: 0 + http_statuses: + - 429 + - 404 + - 500 + - 501 + - 502 + - 503 + - 504 + - 505 + timeouts: 0 + tcp_failures: 0 + interval: 0 + type: http + concurrency: 10 + headers: + x-my-header: + - foo + - bar + x-another-header: + - bla + timeout: 1 + http_path: "/" + https_sni: example.com + threshold: 0 + tags: + - user-level + - low-priority + host_header: example.com + use_srv_name: false + targets: + - target: 10.10.10.10:8000 + weight: 100 + - target: 10.10.10.11:8000 + weight: 200 \ No newline at end of file diff --git a/kong2tf/testdata/upstream-target-output-expected.tf b/kong2tf/testdata/upstream-target-output-expected.tf new file mode 100644 index 000000000..fdb9eb0a1 --- /dev/null +++ b/kong2tf/testdata/upstream-target-output-expected.tf @@ -0,0 +1,77 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_upstream" "upstream_example_api_com" { + name = "example-api.com" + algorithm = "round-robin" + hash_fallback = "none" + hash_on = "none" + hash_on_cookie_path = "/" + healthchecks = { + active = { + concurrency = 10 + headers = { + x-another-header = ["bla"] + x-my-header = ["foo", "bar"] + } + healthy = { + http_statuses = [200, 302] + interval = 0 + successes = 0 + } + http_path = "/" + https_sni = "example.com" + https_verify_certificate = true + timeout = 1 + type = "http" + unhealthy = { + http_failures = 0 + http_statuses = [429, 404, 500, 501, 502, 503, 504, 505] + interval = 0 + tcp_failures = 0 + timeouts = 0 + } + } + passive = { + healthy = { + http_statuses = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] + successes = 0 + } + type = "http" + unhealthy = { + http_failures = 0 + http_statuses = [429, 500, 503] + tcp_failures = 0 + timeouts = 0 + } + } + threshold = 0 + } + host_header = "example.com" + slots = 10000 + tags = ["user-level", "low-priority"] + use_srv_name = false + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_target" "upstream_example_api_com_target_10_10_10_10_8000" { + target = "10.10.10.10:8000" + weight = 100 + + upstream_id = konnect_gateway_upstream.upstream_example_api_com.id + + control_plane_id = var.control_plane_id +} + +resource "konnect_gateway_target" "upstream_example_api_com_target_10_10_10_11_8000" { + target = "10.10.10.11:8000" + weight = 200 + + upstream_id = konnect_gateway_upstream.upstream_example_api_com.id + + control_plane_id = var.control_plane_id +} + diff --git a/kong2tf/testdata/vault-input.yaml b/kong2tf/testdata/vault-input.yaml new file mode 100644 index 000000000..ec9fa862d --- /dev/null +++ b/kong2tf/testdata/vault-input.yaml @@ -0,0 +1,8 @@ +vaults: +- config: + prefix: MY_SECRET_ + description: ENV vault for secrets + name: env + prefix: my-env-vault + tags: + - env-vault \ No newline at end of file diff --git a/kong2tf/testdata/vault-output-expected.tf b/kong2tf/testdata/vault-output-expected.tf new file mode 100644 index 000000000..1c1af8d8b --- /dev/null +++ b/kong2tf/testdata/vault-output-expected.tf @@ -0,0 +1,17 @@ +variable "control_plane_id" { + type = "string" + default = "YOUR_CONTROL_PLANE_ID" +} + +resource "konnect_gateway_vault" "env" { + name = "env" + config = jsonencode({ + prefix = "MY_SECRET_" + }) + description = "ENV vault for secrets" + prefix = "my-env-vault" + tags = ["env-vault"] + + control_plane_id = var.control_plane_id +} +