diff --git a/cmd/convert.go b/cmd/convert.go index 19b794fec..167b2e776 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -54,8 +54,8 @@ can be converted into a 'konnect' configuration file.`, }, } - sourceFormats := []convert.Format{convert.FormatKongGateway} - destinationFormats := []convert.Format{convert.FormatKonnect} + sourceFormats := []convert.Format{convert.FormatKongGateway, convert.FormatKongGateway2x} + destinationFormats := []convert.Format{convert.FormatKonnect, convert.FormatKongGateway3x} convertCmd.Flags().StringVar(&convertCmdSourceFormat, "from", "", fmt.Sprintf("format of the source file, allowed formats: %v", sourceFormats)) convertCmd.Flags().StringVar(&convertCmdDestinationFormat, "to", "", diff --git a/convert/convert.go b/convert/convert.go index 2bbd1744f..376e28cf3 100644 --- a/convert/convert.go +++ b/convert/convert.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/kong/deck/cprint" "github.com/kong/deck/file" "github.com/kong/deck/utils" "github.com/kong/go-kong/kong" @@ -16,6 +17,10 @@ const ( FormatKongGateway Format = "kong-gateway" // FormatKonnect represents the Konnect format. FormatKonnect Format = "konnect" + // FormatKongGateway2x represents the Kong gateway 2.x format. + FormatKongGateway2x Format = "kong-gateway-2.x" + // FormatKongGateway3x represents the Kong gateway 3.x format. + FormatKongGateway3x Format = "kong-gateway-3.x" ) // AllFormats contains all available formats. @@ -28,6 +33,10 @@ func ParseFormat(key string) (Format, error) { return FormatKongGateway, nil case FormatKonnect: return FormatKonnect, nil + case FormatKongGateway2x: + return FormatKongGateway2x, nil + case FormatKongGateway3x: + return FormatKongGateway3x, nil default: return "", fmt.Errorf("invalid format: '%v'", key) } @@ -50,6 +59,11 @@ func Convert(inputFilename, outputFilename string, from, to Format) error { if err != nil { return err } + case from == FormatKongGateway2x && to == FormatKongGateway3x: + outputContent, err = convertKongGateway2xTo3x(inputContent) + if err != nil { + return err + } default: return fmt.Errorf("cannot convert from '%s' to '%s' format", from, to) } @@ -61,6 +75,50 @@ func Convert(inputFilename, outputFilename string, from, to Format) error { return nil } +func convertKongGateway2xTo3x(input *file.Content) (*file.Content, error) { + if input == nil { + return nil, fmt.Errorf("input content is nil") + } + outputContent := input.DeepCopy() + + convertedRoutes := []*file.FRoute{} + for _, service := range outputContent.Services { + for _, route := range service.Routes { + convertedRoutes = append(convertedRoutes, migrateRoutesPathFieldPre300(route)) + } + service.Routes = convertedRoutes + } + + cprint.UpdatePrintf( + "From the config file, the _format_version field has been migrated from '%s' to '%s'.\n"+ + "These automatic changes may not be fully correct or exhaustive, so please\n"+ + "take the time to perform a manual audit of the config file.\n\n"+ + "For related information, please visit: https://docs.konghq.com/gateway/latest/\n", + outputContent.FormatVersion, "3.0") + outputContent.FormatVersion = "3.0" + return outputContent, nil +} + +func migrateRoutesPathFieldPre300(route *file.FRoute) *file.FRoute { + changedPaths := []string{} + for _, path := range route.Paths { + if !strings.HasPrefix(*path, "~/") && utils.IsPathRegexLike(*path) { + changedPaths = append(changedPaths, *path) + *path = "~" + *path + } + } + if len(changedPaths) > 0 { + cprint.UpdatePrintf( + "From the config file, for the route '%s', a regex pattern usage was detected\n"+ + "for the '%s' paths in the 'paths' field.\n"+ + "Kong gateway versions 3.0 and above require that regular expressions\n"+ + "start with a '~' character to distinguish from simple prefix match.\n"+ + "In order to make these paths compatible with 3.x, a '~' prefix has been added.\n\n", + *route.Name, strings.Join(changedPaths, ", ")) + } + return route +} + func convertKongGatewayToKonnect(input *file.Content) (*file.Content, error) { if input == nil { return nil, fmt.Errorf("input content is nil") diff --git a/convert/convert_test.go b/convert/convert_test.go index 6aeab3160..ab341dfc4 100644 --- a/convert/convert_test.go +++ b/convert/convert_test.go @@ -28,6 +28,22 @@ func TestParseFormat(t *testing.T) { want: FormatKongGateway, wantErr: false, }, + { + name: "parses valid values", + args: args{ + key: "kong-gateway-2.x", + }, + want: FormatKongGateway2x, + wantErr: false, + }, + { + name: "parses valid values", + args: args{ + key: "kong-gateway-3.x", + }, + want: FormatKongGateway3x, + wantErr: false, + }, { name: "parses values in a case-insensitive manner", args: args{ @@ -161,6 +177,15 @@ func Test_Convert(t *testing.T) { }, wantErr: true, }, + { + name: "errors out due to invalid conversion", + args: args{ + inputFilename: "testdata/3/input.yaml", + fromFormat: FormatKongGateway3x, + toFormat: FormatKongGateway2x, + }, + wantErr: true, + }, { name: "errors out when a nameless service is present in the input", args: args{ @@ -190,6 +215,28 @@ func Test_Convert(t *testing.T) { }, wantErr: false, }, + { + name: "converts from Kong Gateway 2.x to Kong Gateway 3.x format", + args: args{ + inputFilename: "testdata/3/input.yaml", + outputFilename: "testdata/3/output.yaml", + expectedOutputFilename: "testdata/3/output-expected.yaml", + fromFormat: FormatKongGateway2x, + toFormat: FormatKongGateway3x, + }, + wantErr: false, + }, + { + name: "converts from Kong Gateway 2.x to Kong Gateway 3.x format (no _format_version input)", + args: args{ + inputFilename: "testdata/4/input.yaml", + outputFilename: "testdata/4/output.yaml", + expectedOutputFilename: "testdata/4/output-expected.yaml", + fromFormat: FormatKongGateway2x, + toFormat: FormatKongGateway3x, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/convert/testdata/3/input.yaml b/convert/testdata/3/input.yaml new file mode 100644 index 000000000..a790dfe7b --- /dev/null +++ b/convert/testdata/3/input.yaml @@ -0,0 +1,9 @@ +services: +- name: svc1 + host: mockbin.org + path: /status/200 + routes: + - name: r1 + paths: + - /status/\d+ + - /code/\d+ diff --git a/convert/testdata/3/output-expected.yaml b/convert/testdata/3/output-expected.yaml new file mode 100644 index 000000000..a9e4830ba --- /dev/null +++ b/convert/testdata/3/output-expected.yaml @@ -0,0 +1,10 @@ +_format_version: "3.0" +services: +- name: svc1 + host: mockbin.org + path: /status/200 + routes: + - name: r1 + paths: + - ~/status/\d+ + - ~/code/\d+ diff --git a/convert/testdata/3/output.yaml b/convert/testdata/3/output.yaml new file mode 100644 index 000000000..eafd39703 --- /dev/null +++ b/convert/testdata/3/output.yaml @@ -0,0 +1,10 @@ +_format_version: "3.0" +services: +- host: mockbin.org + name: svc1 + path: /status/200 + routes: + - name: r1 + paths: + - ~/status/\d+ + - ~/code/\d+ diff --git a/convert/testdata/4/input.yaml b/convert/testdata/4/input.yaml new file mode 100644 index 000000000..809701880 --- /dev/null +++ b/convert/testdata/4/input.yaml @@ -0,0 +1,10 @@ +_format_version: "1.1" +services: +- name: svc1 + host: mockbin.org + path: /status/200 + routes: + - name: r1 + paths: + - /status/\d+ + - /code/\d+ diff --git a/convert/testdata/4/output-expected.yaml b/convert/testdata/4/output-expected.yaml new file mode 100644 index 000000000..a9e4830ba --- /dev/null +++ b/convert/testdata/4/output-expected.yaml @@ -0,0 +1,10 @@ +_format_version: "3.0" +services: +- name: svc1 + host: mockbin.org + path: /status/200 + routes: + - name: r1 + paths: + - ~/status/\d+ + - ~/code/\d+ diff --git a/convert/testdata/4/output.yaml b/convert/testdata/4/output.yaml new file mode 100644 index 000000000..eafd39703 --- /dev/null +++ b/convert/testdata/4/output.yaml @@ -0,0 +1,10 @@ +_format_version: "3.0" +services: +- host: mockbin.org + name: svc1 + path: /status/200 + routes: + - name: r1 + paths: + - ~/status/\d+ + - ~/code/\d+