From ee542fd5de3d55ec97d43cd9d181023ff2ba6136 Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Tue, 7 Apr 2026 09:33:02 +0100 Subject: [PATCH 1/2] feat: add 'deck file format' command for deck/dbless conversion Adds a new 'deck file format' subcommand that converts Kong configuration files between decK format and Kong DBless format. Usage: decK DBless The two formats differ in consumer-group entity representation: - decK: plugins nested under consumer_groups[*].plugins, memberships nested under consumers[*].groups - DBless: plugins in top-level consumer_group_plugins array, memberships in top-level consumer_group_consumers array decK, delegates to deckformat.ConvertDBless from go-apiops. DBless, implements the inverse transformation locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cmd/file_format.go | 179 ++++++++++++++++++ cmd/root.go | 1 + tests/integration/file_format_test.go | 128 +++++++++++++ tests/integration/test_utils.go | 22 +++ .../testdata/file-format/dbless-input.yaml | 27 +++ .../testdata/file-format/deck-input.yaml | 23 +++ .../file-format/no-consumer-groups.yaml | 11 ++ 7 files changed, 391 insertions(+) create mode 100644 cmd/file_format.go create mode 100644 tests/integration/file_format_test.go create mode 100644 tests/integration/testdata/file-format/dbless-input.yaml create mode 100644 tests/integration/testdata/file-format/deck-input.yaml create mode 100644 tests/integration/testdata/file-format/no-consumer-groups.yaml diff --git a/cmd/file_format.go b/cmd/file_format.go new file mode 100644 index 000000000..7385672b8 --- /dev/null +++ b/cmd/file_format.go @@ -0,0 +1,179 @@ +package cmd + +import ( + "fmt" + "log" + "strings" + + "github.com/kong/go-apiops/deckformat" + "github.com/kong/go-apiops/filebasics" + "github.com/kong/go-apiops/jsonbasics" + "github.com/kong/go-apiops/logbasics" + "github.com/spf13/cobra" +) + +var ( + cmdFileFormatOutputFilename string + cmdFileFormatOutputFormat string +) + +const ( + fileFormatTypeDeck = "deck" + fileFormatTypeDBless = "dbless" +) + +// convertDeckToDBless converts a decK format file to DBless format. +// It is the inverse of deckformat.ConvertDBless. +// +// The following transformations are applied: +// - consumer_groups[*].plugins → top-level consumer_group_plugins (with consumer_group field) +// - consumers[*].groups → top-level consumer_group_consumers (with consumer and consumer_group fields) +func convertDeckToDBless(data map[string]interface{}) (map[string]interface{}, error) { + // Step 1: Extract consumer_groups[*].plugins into top-level consumer_group_plugins. + consumerGroups, err := jsonbasics.GetObjectArrayField(data, "consumer_groups") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumer_groups'; %w", err) + } + + var consumerGroupPlugins []map[string]interface{} + for i, consumerGroup := range consumerGroups { + groupName, err := jsonbasics.GetStringField(consumerGroup, "name") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumer_groups[%d].name'; %w", i, err) + } + + plugins, err := jsonbasics.GetObjectArrayField(consumerGroup, "plugins") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumer_groups[%d].plugins'; %w", i, err) + } + + for _, plugin := range plugins { + plugin["consumer_group"] = groupName + consumerGroupPlugins = append(consumerGroupPlugins, plugin) + } + // Remove nested plugins from the consumer_group entry. + jsonbasics.SetObjectArrayField(consumerGroup, "plugins", nil) + } + + if len(consumerGroupPlugins) > 0 { + jsonbasics.SetObjectArrayField(data, "consumer_group_plugins", consumerGroupPlugins) + } + + // Step 2: Extract consumers[*].groups into top-level consumer_group_consumers. + consumers, err := jsonbasics.GetObjectArrayField(data, "consumers") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumers'; %w", err) + } + + var consumerGroupConsumers []map[string]interface{} + for i, consumer := range consumers { + username, err := jsonbasics.GetStringField(consumer, "username") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumers[%d].username'; %w", i, err) + } + + groups, err := jsonbasics.GetObjectArrayField(consumer, "groups") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumers[%d].groups'; %w", i, err) + } + + for j, group := range groups { + groupName, err := jsonbasics.GetStringField(group, "name") + if err != nil { + return nil, fmt.Errorf("failed to read 'consumers[%d].groups[%d].name'; %w", i, j, err) + } + entry := map[string]interface{}{ + "consumer": username, + "consumer_group": groupName, + } + consumerGroupConsumers = append(consumerGroupConsumers, entry) + } + // Remove nested groups from the consumer entry. + jsonbasics.SetObjectArrayField(consumer, "groups", nil) + } + + if len(consumerGroupConsumers) > 0 { + jsonbasics.SetObjectArrayField(data, "consumer_group_consumers", consumerGroupConsumers) + } + + return data, nil +} + +// executeFileFormat is the handler for the "file format" command. +func executeFileFormat(cmd *cobra.Command, args []string) error { + verbosity, _ := cmd.Flags().GetInt("verbose") + logbasics.Initialize(log.LstdFlags, verbosity) + _ = sendAnalytics("file-format", "", modeLocal) + + cmdFileFormatOutputFormat = strings.ToUpper(cmdFileFormatOutputFormat) + + formatType := args[0] + inputFilename := args[1] + + data, err := filebasics.DeserializeFile(inputFilename) + if err != nil { + return fmt.Errorf("failed to read input file '%s'; %w", inputFilename, err) + } + + switch formatType { + case fileFormatTypeDeck: + data, err = deckformat.ConvertDBless(data) + if err != nil { + return fmt.Errorf("failed to convert DBless to decK format; %w", err) + } + case fileFormatTypeDBless: + data, err = convertDeckToDBless(data) + if err != nil { + return fmt.Errorf("failed to convert decK to DBless format; %w", err) + } + } + + trackInfo := deckformat.HistoryNewEntry("format") + trackInfo["input"] = inputFilename + trackInfo["output"] = cmdFileFormatOutputFilename + trackInfo["type"] = formatType + deckformat.HistoryAppend(data, trackInfo) + + return filebasics.WriteSerializedFile( + cmdFileFormatOutputFilename, + data, + filebasics.OutputFormat(cmdFileFormatOutputFormat)) +} + +// newFileFormatCmd returns the cobra command for "deck file format". +func newFileFormatCmd() *cobra.Command { + formatCmd := &cobra.Command{ + Use: fmt.Sprintf("format [flags] %s|%s filename", fileFormatTypeDeck, fileFormatTypeDBless), + Short: "Convert between decK and DBless file formats", + Long: `Convert Kong configuration files between decK and Kong DBless formats. + +The two formats differ in how consumer-group related entities are represented: + - decK format: consumer group plugins are nested under consumer_groups[*].plugins, + and consumer group memberships are nested under consumers[*].groups. + - DBless format: consumer group plugins are stored in a top-level consumer_group_plugins + array, and memberships are stored in a top-level consumer_group_consumers + array. + +Use 'deck' as the type to convert a DBless file into decK format. +Use 'dbless' as the type to convert a decK file into DBless format.`, + Example: "# Convert a DBless file to decK format\n" + + "deck file format deck dbless.yaml\n\n" + + "# Convert a decK file to DBless format\n" + + "deck file format dbless deck.yaml", + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.ExactArgs(2)(cmd, args); err != nil { + return err + } + validTypes := []string{fileFormatTypeDeck, fileFormatTypeDBless} + return validateInputFlag("type", args[0], validTypes, "") + }, + RunE: executeFileFormat, + } + + formatCmd.Flags().StringVarP(&cmdFileFormatOutputFilename, "output-file", "o", "-", + "Output file to write to. Use - to write to stdout.") + formatCmd.Flags().StringVar(&cmdFileFormatOutputFormat, "format", "yaml", + "Output file format: yaml or json.") + + return formatCmd +} diff --git a/cmd/root.go b/cmd/root.go index 05fbd2fbd..e91cd2619 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -266,6 +266,7 @@ It can be used to export, import, or sync entities to Kong.`, fileCmd.AddCommand(newValidateCmd(false, false)) // file-based validation fileCmd.AddCommand(newKong2KicCmd()) fileCmd.AddCommand(newKong2TfCmd()) + fileCmd.AddCommand(newFileFormatCmd()) } return rootCmd } diff --git a/tests/integration/file_format_test.go b/tests/integration/file_format_test.go new file mode 100644 index 000000000..ca9bf4699 --- /dev/null +++ b/tests/integration/file_format_test.go @@ -0,0 +1,128 @@ +package integration + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +func Test_FileFormat_DBlessToDeck(t *testing.T) { + tests := []struct { + name string + inputFile string + expectedOutputFile string + errorExpected bool + errorString string + }{ + { + name: "converts DBless consumer groups to decK format", + inputFile: "testdata/file-format/dbless-input.yaml", + expectedOutputFile: "testdata/file-format/deck-input.yaml", + }, + { + name: "file with no consumer groups passes through unchanged", + inputFile: "testdata/file-format/no-consumer-groups.yaml", + expectedOutputFile: "testdata/file-format/no-consumer-groups.yaml", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + output, err := fileFormat("deck", tc.inputFile) + + if tc.errorExpected { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorString) + return + } + + require.NoError(t, err) + + content, err := os.ReadFile(tc.expectedOutputFile) + require.NoError(t, err) + + var expected, actual interface{} + require.NoError(t, yaml.Unmarshal(content, &expected)) + require.NoError(t, yaml.Unmarshal([]byte(output), &actual)) + assert.Equal(t, expected, actual) + }) + } +} + +func Test_FileFormat_DeckToDBless(t *testing.T) { + tests := []struct { + name string + inputFile string + expectedOutputFile string + errorExpected bool + errorString string + }{ + { + name: "converts decK consumer groups to DBless format", + inputFile: "testdata/file-format/deck-input.yaml", + expectedOutputFile: "testdata/file-format/dbless-input.yaml", + }, + { + name: "file with no consumer groups passes through unchanged", + inputFile: "testdata/file-format/no-consumer-groups.yaml", + expectedOutputFile: "testdata/file-format/no-consumer-groups.yaml", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + output, err := fileFormat("dbless", tc.inputFile) + + if tc.errorExpected { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorString) + return + } + + require.NoError(t, err) + + content, err := os.ReadFile(tc.expectedOutputFile) + require.NoError(t, err) + + var expected, actual interface{} + require.NoError(t, yaml.Unmarshal(content, &expected)) + require.NoError(t, yaml.Unmarshal([]byte(output), &actual)) + assert.Equal(t, expected, actual) + }) + } +} + +func Test_FileFormat_InvalidArgs(t *testing.T) { + tests := []struct { + name string + args []string + errorString string + }{ + { + name: "invalid type argument", + args: []string{"invalid-type", "testdata/file-format/deck-input.yaml"}, + errorString: "invalid value 'invalid-type' found for the 'type' flag", + }, + { + name: "missing file argument", + args: []string{"deck"}, + errorString: "accepts 2 arg(s), received 1", + }, + { + name: "no arguments", + args: []string{}, + errorString: "accepts 2 arg(s), received 0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := fileFormat(tc.args...) + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorString) + }) + } +} diff --git a/tests/integration/test_utils.go b/tests/integration/test_utils.go index 37059737f..82d936329 100644 --- a/tests/integration/test_utils.go +++ b/tests/integration/test_utils.go @@ -426,6 +426,28 @@ func fileLint(opts ...string) (string, error) { return stripansi.Strip(string(out)), cmdErr } +func fileFormat(opts ...string) (string, error) { + deckCmd := cmd.NewRootCmd() + args := []string{"file", "format"} + if len(opts) > 0 { + args = append(args, opts...) + } + deckCmd.SetArgs(args) + + // capture command output to be used during tests + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + cmdErr := deckCmd.ExecuteContext(context.Background()) + + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + + return stripansi.Strip(string(out)), cmdErr +} + func fileConvert(opts ...string) (string, error) { deckCmd := cmd.NewRootCmd() args := []string{"file", "convert"} diff --git a/tests/integration/testdata/file-format/dbless-input.yaml b/tests/integration/testdata/file-format/dbless-input.yaml new file mode 100644 index 000000000..51cd86ba5 --- /dev/null +++ b/tests/integration/testdata/file-format/dbless-input.yaml @@ -0,0 +1,27 @@ +_format_version: "3.0" +_transform: false + +consumer_groups: +- name: A-team + +consumer_group_plugins: +- name: rate-limiting-advanced + consumer_group: A-team + config: + limit: + - 1000 + window_size: + - 3600 + window_type: sliding + +consumers: +- username: tieske + custom_id: tieske-custom +- username: foo + custom_id: bar + +consumer_group_consumers: +- consumer: tieske + consumer_group: A-team +- consumer: foo + consumer_group: A-team diff --git a/tests/integration/testdata/file-format/deck-input.yaml b/tests/integration/testdata/file-format/deck-input.yaml new file mode 100644 index 000000000..a9b6e2104 --- /dev/null +++ b/tests/integration/testdata/file-format/deck-input.yaml @@ -0,0 +1,23 @@ +_format_version: "3.0" +_transform: false + +consumer_groups: +- name: A-team + plugins: + - name: rate-limiting-advanced + config: + limit: + - 1000 + window_size: + - 3600 + window_type: sliding + +consumers: +- username: tieske + custom_id: tieske-custom + groups: + - name: A-team +- username: foo + custom_id: bar + groups: + - name: A-team diff --git a/tests/integration/testdata/file-format/no-consumer-groups.yaml b/tests/integration/testdata/file-format/no-consumer-groups.yaml new file mode 100644 index 000000000..772f2cb06 --- /dev/null +++ b/tests/integration/testdata/file-format/no-consumer-groups.yaml @@ -0,0 +1,11 @@ +_format_version: "3.0" + +services: +- name: example-service + host: example.com + port: 80 + protocol: http + routes: + - name: example-route + paths: + - /example From 27f8e30a856013e8b78803972bfef1347ee6a24d Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Fri, 8 May 2026 17:14:47 +0100 Subject: [PATCH 2/2] Add Partials support --- cmd/file_format.go | 157 +++++++++++++++++- tests/integration/file_format_test.go | 10 ++ .../file-format/dbless-partials-input.yaml | 28 ++++ .../file-format/deck-partials-input.yaml | 27 +++ 4 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 tests/integration/testdata/file-format/dbless-partials-input.yaml create mode 100644 tests/integration/testdata/file-format/deck-partials-input.yaml diff --git a/cmd/file_format.go b/cmd/file_format.go index 7385672b8..5900614bd 100644 --- a/cmd/file_format.go +++ b/cmd/file_format.go @@ -28,6 +28,7 @@ const ( // The following transformations are applied: // - consumer_groups[*].plugins → top-level consumer_group_plugins (with consumer_group field) // - consumers[*].groups → top-level consumer_group_consumers (with consumer and consumer_group fields) +// - plugins[*].partials → top-level plugins_partials (with plugin/partial/path fields) func convertDeckToDBless(data map[string]interface{}) (map[string]interface{}, error) { // Step 1: Extract consumer_groups[*].plugins into top-level consumer_group_plugins. consumerGroups, err := jsonbasics.GetObjectArrayField(data, "consumer_groups") @@ -96,9 +97,156 @@ func convertDeckToDBless(data map[string]interface{}) (map[string]interface{}, e jsonbasics.SetObjectArrayField(data, "consumer_group_consumers", consumerGroupConsumers) } + // Step 3: Extract plugins[*].partials into top-level plugins_partials. + plugins, err := jsonbasics.GetObjectArrayField(data, "plugins") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins'; %w", err) + } + + var pluginPartials []map[string]interface{} + for i, plugin := range plugins { + pluginRef, err := jsonbasics.GetStringField(plugin, "id") + if err != nil { + pluginRef, err = jsonbasics.GetStringField(plugin, "name") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins[%d].id' or 'plugins[%d].name'; %w", i, i, err) + } + } + + partials, err := jsonbasics.GetObjectArrayField(plugin, "partials") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins[%d].partials'; %w", i, err) + } + + for j, partial := range partials { + partialRef, err := jsonbasics.GetStringField(partial, "id") + if err != nil { + partialRef, err = jsonbasics.GetStringField(partial, "name") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins[%d].partials[%d].id' or 'plugins[%d].partials[%d].name'; %w", i, j, i, j, err) + } + } + + entry := map[string]interface{}{ + "plugin": pluginRef, + "partial": partialRef, + } + + if path, err := jsonbasics.GetStringField(partial, "path"); err == nil { + entry["path"] = path + } + + pluginPartials = append(pluginPartials, entry) + } + + // Remove nested partials from the plugin entry. + jsonbasics.SetObjectArrayField(plugin, "partials", nil) + } + + if len(pluginPartials) > 0 { + jsonbasics.SetObjectArrayField(data, "plugins_partials", pluginPartials) + } + return data, nil } +func convertDBlessToDeck(data map[string]interface{}) (map[string]interface{}, error) { + converted, err := deckformat.ConvertDBless(data) + if err != nil { + return nil, err + } + + pluginsPartials, err := jsonbasics.GetObjectArrayField(converted, "plugins_partials") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins_partials'; %w", err) + } + if len(pluginsPartials) == 0 { + return converted, nil + } + + plugins, err := jsonbasics.GetObjectArrayField(converted, "plugins") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins'; %w", err) + } + + partials, err := jsonbasics.GetObjectArrayField(converted, "partials") + if err != nil { + return nil, fmt.Errorf("failed to read 'partials'; %w", err) + } + + partialsByID := make(map[string]map[string]interface{}) + partialsByName := make(map[string]map[string]interface{}) + for _, partial := range partials { + if id, err := jsonbasics.GetStringField(partial, "id"); err == nil { + partialsByID[id] = partial + } + if name, err := jsonbasics.GetStringField(partial, "name"); err == nil { + partialsByName[name] = partial + } + } + + findPlugin := func(ref string) map[string]interface{} { + for _, plugin := range plugins { + if id, err := jsonbasics.GetStringField(plugin, "id"); err == nil && id == ref { + return plugin + } + if name, err := jsonbasics.GetStringField(plugin, "name"); err == nil && name == ref { + return plugin + } + } + return nil + } + + for i, pluginPartial := range pluginsPartials { + pluginRef, err := jsonbasics.GetStringField(pluginPartial, "plugin") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins_partials[%d].plugin'; %w", i, err) + } + plugin := findPlugin(pluginRef) + if plugin == nil { + return nil, fmt.Errorf("failed to resolve 'plugins_partials[%d].plugin'='%s' to a plugin in the file", i, pluginRef) + } + + partialRef, err := jsonbasics.GetStringField(pluginPartial, "partial") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins_partials[%d].partial'; %w", i, err) + } + + partialEntry := map[string]interface{}{} + if partial, ok := partialsByID[partialRef]; ok { + if id, err := jsonbasics.GetStringField(partial, "id"); err == nil { + partialEntry["id"] = id + } + if name, err := jsonbasics.GetStringField(partial, "name"); err == nil { + partialEntry["name"] = name + } + } else if partial, ok := partialsByName[partialRef]; ok { + if id, err := jsonbasics.GetStringField(partial, "id"); err == nil { + partialEntry["id"] = id + } + if name, err := jsonbasics.GetStringField(partial, "name"); err == nil { + partialEntry["name"] = name + } + } else { + partialEntry["id"] = partialRef + } + + if path, err := jsonbasics.GetStringField(pluginPartial, "path"); err == nil { + partialEntry["path"] = path + } + + pluginPartialsEntry, err := jsonbasics.GetObjectArrayField(plugin, "partials") + if err != nil { + return nil, fmt.Errorf("failed to read 'plugins[%d].partials'; %w", i, err) + } + pluginPartialsEntry = append(pluginPartialsEntry, partialEntry) + jsonbasics.SetObjectArrayField(plugin, "partials", pluginPartialsEntry) + } + + jsonbasics.SetObjectArrayField(converted, "plugins_partials", nil) + return converted, nil +} + // executeFileFormat is the handler for the "file format" command. func executeFileFormat(cmd *cobra.Command, args []string) error { verbosity, _ := cmd.Flags().GetInt("verbose") @@ -117,7 +265,7 @@ func executeFileFormat(cmd *cobra.Command, args []string) error { switch formatType { case fileFormatTypeDeck: - data, err = deckformat.ConvertDBless(data) + data, err = convertDBlessToDeck(data) if err != nil { return fmt.Errorf("failed to convert DBless to decK format; %w", err) } @@ -149,10 +297,11 @@ func newFileFormatCmd() *cobra.Command { The two formats differ in how consumer-group related entities are represented: - decK format: consumer group plugins are nested under consumer_groups[*].plugins, - and consumer group memberships are nested under consumers[*].groups. + consumer group memberships are nested under consumers[*].groups, + and plugin partial links are nested under plugins[*].partials. - DBless format: consumer group plugins are stored in a top-level consumer_group_plugins - array, and memberships are stored in a top-level consumer_group_consumers - array. + array, memberships are stored in a top-level consumer_group_consumers + array, and plugin partial links are stored in top-level plugins_partials. Use 'deck' as the type to convert a DBless file into decK format. Use 'dbless' as the type to convert a decK file into DBless format.`, diff --git a/tests/integration/file_format_test.go b/tests/integration/file_format_test.go index ca9bf4699..5a48ae710 100644 --- a/tests/integration/file_format_test.go +++ b/tests/integration/file_format_test.go @@ -22,6 +22,11 @@ func Test_FileFormat_DBlessToDeck(t *testing.T) { inputFile: "testdata/file-format/dbless-input.yaml", expectedOutputFile: "testdata/file-format/deck-input.yaml", }, + { + name: "converts DBless plugin partial links to decK format", + inputFile: "testdata/file-format/dbless-partials-input.yaml", + expectedOutputFile: "testdata/file-format/deck-partials-input.yaml", + }, { name: "file with no consumer groups passes through unchanged", inputFile: "testdata/file-format/no-consumer-groups.yaml", @@ -65,6 +70,11 @@ func Test_FileFormat_DeckToDBless(t *testing.T) { inputFile: "testdata/file-format/deck-input.yaml", expectedOutputFile: "testdata/file-format/dbless-input.yaml", }, + { + name: "converts decK plugin partial links to DBless format", + inputFile: "testdata/file-format/deck-partials-input.yaml", + expectedOutputFile: "testdata/file-format/dbless-partials-input.yaml", + }, { name: "file with no consumer groups passes through unchanged", inputFile: "testdata/file-format/no-consumer-groups.yaml", diff --git a/tests/integration/testdata/file-format/dbless-partials-input.yaml b/tests/integration/testdata/file-format/dbless-partials-input.yaml new file mode 100644 index 000000000..2277495d8 --- /dev/null +++ b/tests/integration/testdata/file-format/dbless-partials-input.yaml @@ -0,0 +1,28 @@ +_format_version: "3.0" +_transform: false + +partials: +- id: c4ff03ed-478a-4b86-bdcf-811c45c00737 + name: my-redis-instance + type: redis-ee + config: + port: 6379 + host: 127.0.0.1 + +plugins: +- id: 017320fe-ba14-4e14-8bac-3a7d1d3cd1e0 + name: rate-limiting-advanced + config: + strategy: redis + sync_rate: -1 + limit: + - 1000 + retry_after_jitter_max: 0 + window_size: + - 3600 + window_type: sliding + +plugins_partials: +- plugin: 017320fe-ba14-4e14-8bac-3a7d1d3cd1e0 + partial: c4ff03ed-478a-4b86-bdcf-811c45c00737 + path: config.redis diff --git a/tests/integration/testdata/file-format/deck-partials-input.yaml b/tests/integration/testdata/file-format/deck-partials-input.yaml new file mode 100644 index 000000000..b01e47337 --- /dev/null +++ b/tests/integration/testdata/file-format/deck-partials-input.yaml @@ -0,0 +1,27 @@ +_format_version: "3.0" +_transform: false + +partials: +- id: c4ff03ed-478a-4b86-bdcf-811c45c00737 + name: my-redis-instance + type: redis-ee + config: + port: 6379 + host: 127.0.0.1 + +plugins: +- id: 017320fe-ba14-4e14-8bac-3a7d1d3cd1e0 + name: rate-limiting-advanced + config: + strategy: redis + sync_rate: -1 + limit: + - 1000 + retry_after_jitter_max: 0 + window_size: + - 3600 + window_type: sliding + partials: + - id: c4ff03ed-478a-4b86-bdcf-811c45c00737 + name: my-redis-instance + path: config.redis