From d7510eada29ad95712425dcd214e8599a9f2c202 Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Tue, 7 Mar 2023 21:39:06 -0500 Subject: [PATCH] Change kustomize edit fix to split patchesStrategicMerge entries before converting them to patch entries. --- api/types/kustomization.go | 149 +++++++- api/types/kustomization_test.go | 651 ++++++++++++++++++++++++++++++++ kyaml/kio/byteio_reader.go | 19 +- 3 files changed, 798 insertions(+), 21 deletions(-) diff --git a/api/types/kustomization.go b/api/types/kustomization.go index e0dcdcf218c..dcf6231c8e7 100644 --- a/api/types/kustomization.go +++ b/api/types/kustomization.go @@ -5,9 +5,12 @@ package types import ( "fmt" + "path/filepath" + "strings" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/filesys" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/yaml" ) @@ -263,18 +266,9 @@ func (k *Kustomization) FixKustomizationPreMarshalling(fSys filesys.FileSystem) k.Patches = append(k.Patches, k.PatchesJson6902...) k.PatchesJson6902 = nil - if k.PatchesStrategicMerge != nil { - for _, patchStrategicMerge := range k.PatchesStrategicMerge { - // check this patch is file path select. - if _, err := fSys.ReadFile(string(patchStrategicMerge)); err == nil { - // path patch - k.Patches = append(k.Patches, Patch{Path: string(patchStrategicMerge)}) - } else { - // inline string patch - k.Patches = append(k.Patches, Patch{Patch: string(patchStrategicMerge)}) - } - } - k.PatchesStrategicMerge = nil + // Convert patchesStrategicMerge to patches. + if err := k.fixPatchesStrategicMerge(fSys); err != nil { + return err } // this fix is not in FixKustomizationPostUnmarshalling because @@ -296,6 +290,39 @@ func (k *Kustomization) FixKustomizationPreMarshalling(fSys filesys.FileSystem) return nil } +// fixPatchesStrategicMerge converts PatchesStrategicMerge to Patches. +func (k *Kustomization) fixPatchesStrategicMerge(fSys filesys.FileSystem) error { + if k.PatchesStrategicMerge == nil { + return nil + } + + for _, patchStrategicMerge := range k.PatchesStrategicMerge { + // check this patch is file path select. + if _, err := fSys.ReadFile(string(patchStrategicMerge)); err == nil { + // path patch + patches, err := splitPatchStrategicMerge(patchStrategicMerge, fSys) + if err != nil { + return err + } + for _, patch := range patches { + k.Patches = append(k.Patches, Patch{Path: string(patch)}) + } + } else { + // inline string patch + patches, err := splitInlinePatchStrategicMerge(patchStrategicMerge) + if err != nil { + return err + } + for _, patch := range patches { + k.Patches = append(k.Patches, Patch{Patch: string(patch)}) + } + } + } + k.PatchesStrategicMerge = nil + + return nil +} + func (k *Kustomization) EnforceFields() []string { var errs []string if k.Kind != "" && k.Kind != KustomizationKind && k.Kind != ComponentKind { @@ -318,3 +345,101 @@ func (k *Kustomization) Unmarshal(y []byte) error { } return nil } + +// splitPatchStrategicMerge splits a single PatchStrategicMerge file into multiple PatchStrategicMerge files, if the +// file contains multiple documents separated by the yaml separator. +func splitPatchStrategicMerge(p PatchStrategicMerge, fSys filesys.FileSystem) ([]PatchStrategicMerge, error) { + patchContentBytes, err := fSys.ReadFile(string(p)) + if err != nil { + return nil, fmt.Errorf("failed to read file %v: %w", p, err) + } + + splitPatchContent, err := kio.SplitDocuments(string(patchContentBytes)) + if err != nil { + return nil, fmt.Errorf("failed to split patch file %v: %w", p, err) + } + + // If the split resulted in only one document, there is nothing to do, so just return the original patch without any changes. + if len(splitPatchContent) == 1 { + return []PatchStrategicMerge{p}, nil + } + + // Find the new patches, removing any empty ones. + var newPatches []string + for _, pc := range splitPatchContent { + trimmedPatchContent := strings.TrimSpace(pc) + if len(trimmedPatchContent) > 0 { + newPatches = append(newPatches, trimmedPatchContent+"\n") + } + } + + // If there is only one new patch, that means there was one or more empty ones that were discarded. In this case, + // overwrite the original patch file with the new patch content and return it. + if len(newPatches) == 1 { + err := fSys.WriteFile(string(p), []byte(newPatches[0])) + if err != nil { + return nil, fmt.Errorf("failed to write file %v: %w", p, err) + } + return []PatchStrategicMerge{p}, nil + } + + // If there are multiple new patches, create new patch files for each one, remove the original patch file, and + // return the list of new patch files. + result := make([]PatchStrategicMerge, len(newPatches)) + for i, newPatchContent := range newPatches { + newPatchPath, err := availableFilename(string(p), i+1, fSys) + if err != nil { + return nil, fmt.Errorf("failed to find available filename for %v: %w", p, err) + } + err = fSys.WriteFile(newPatchPath, []byte(newPatchContent)) + if err != nil { + return nil, fmt.Errorf("failed to write file %v: %w", newPatchPath, err) + } + result[i] = PatchStrategicMerge(newPatchPath) + } + + err = fSys.RemoveAll(string(p)) + if err != nil { + return nil, fmt.Errorf("failed to remove file %v: %w", p, err) + } + + return result, nil +} + +// splitInlinePatchStrategicMerge splits a single inline PatchStrategicMerge into multiple inline PatchStrategicMerges, +// if it contains multiple documents separated by the yaml separator. +func splitInlinePatchStrategicMerge(p PatchStrategicMerge) ([]PatchStrategicMerge, error) { + splitPatchContent, err := kio.SplitDocuments(string(p)) + if err != nil { + return nil, fmt.Errorf("failed to split inline patch: %w", err) + } + + // If the split resulted in only one document, there is nothing to do, so just return the original patch without any changes. + if len(splitPatchContent) == 1 { + return []PatchStrategicMerge{p}, nil + } + + // Find the new patches, removing any empty ones. + var newPatches []PatchStrategicMerge + for _, pc := range splitPatchContent { + trimmedPatchContent := strings.TrimSpace(pc) + if len(trimmedPatchContent) > 0 { + newPatches = append(newPatches, PatchStrategicMerge(trimmedPatchContent+"\n")) + } + } + return newPatches, nil +} + +// availableFilename returns a filename that does not already exist in the filesystem, by repeatedly appending a suffix +// to the filename until a non-existing filename is found. +func availableFilename(originalFilename string, suffix int, fSys filesys.FileSystem) (string, error) { + ext := filepath.Ext(originalFilename) + base := strings.TrimSuffix(originalFilename, ext) + for i := 0; i < 100; i++ { + base += fmt.Sprintf("-%d", suffix) + if !fSys.Exists(base + ext) { + return base + ext, nil + } + } + return "", fmt.Errorf("unable to find available filename for %s and suffix %d", originalFilename, suffix) +} diff --git a/api/types/kustomization_test.go b/api/types/kustomization_test.go index ff6e9a68578..5cb74832ec7 100644 --- a/api/types/kustomization_test.go +++ b/api/types/kustomization_test.go @@ -6,6 +6,10 @@ package types import ( "reflect" "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/filesys" ) func fixKustomizationPostUnmarshallingCheck(k, e *Kustomization) bool { @@ -295,3 +299,650 @@ unknown`) t.Fatalf("expect an error") } } + +func TestFixKustomizationPreMarshalling_SplitPatches(t *testing.T) { + testCases := map[string]struct { + kustomization Kustomization + files map[string]string + expectedKustomization Kustomization + expectedFiles map[string]string + }{ + "no split needed": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "secret.patch.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch.yaml": ` +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + }, + }, + + "no split needed (inline)": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{` +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`}, + }, + files: map[string]string{}, + expectedKustomization: Kustomization{ + Patches: []Patch{{Patch: ` +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`}}, + }, + expectedFiles: map[string]string{}, + }, + + "remove unnecessary document separators": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +--- # Some comment +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +--- +--- +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "secret.patch.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + }, + }, + + "remove unnecessary document separators (inline)": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{` +--- # Some comment +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +--- +--- +`}, + }, + files: map[string]string{}, + expectedKustomization: Kustomization{ + Patches: []Patch{{Patch: `apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`}}, + }, + expectedFiles: map[string]string{}, + }, + + "split into two patches": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "secret.patch-1.yaml"}, {Path: "secret.patch-2.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + }, + }, + + "split into two patches (inline)": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`}, + }, + files: map[string]string{}, + expectedKustomization: Kustomization{ + Patches: []Patch{{Patch: `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`}, {Patch: `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`}}, + }, + expectedFiles: map[string]string{}, + }, + + "split should not affect existing patch": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + Patches: []Patch{{Path: "existing.patch.yaml"}}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "existing.patch.yaml": ` +apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "existing.patch.yaml"}, {Path: "secret.patch-1.yaml"}, {Path: "secret.patch-2.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "existing.patch.yaml": ` +apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +`, + }, + }, + + "split into two patches handle filename collision": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "secret.patch-1.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2-2-2.yaml": `# I'm here to cause a filename collision`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "secret.patch-1-1.yaml"}, {Path: "secret.patch-2-2-2-2-2.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch-1-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2-2-2-2-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "secret.patch-1.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2-2.yaml": `# I'm here to cause a filename collision`, + "secret.patch-2-2-2-2.yaml": `# I'm here to cause a filename collision`, + }, + }, + + "split into two patches and handle unnecessary document separators": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +--- +--- +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +--- # Something here +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +--- + +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{{Path: "secret.patch-1.yaml"}, {Path: "secret.patch-2.yaml"}}, + }, + expectedFiles: map[string]string{ + "secret.patch-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + }, + }, + + "split multiple patches into multiple patches": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml", "foo.patch.yaml", "bar.patch.yaml"}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "foo.patch.yaml": ` +apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo2 +spec: something +`, + "bar.patch.yaml": ` +apiVersion: v1 +kind: Bar +metadata: + name: bar1 +spec: something +--- +apiVersion: v1 +kind: Bar +metadata: + name: bar2 +spec: something +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{ + {Path: "secret.patch-1.yaml"}, + {Path: "secret.patch-2.yaml"}, + {Path: "foo.patch-1.yaml"}, + {Path: "foo.patch-2.yaml"}, + {Path: "bar.patch-1.yaml"}, + {Path: "bar.patch-2.yaml"}, + }, + }, + expectedFiles: map[string]string{ + "secret.patch-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "foo.patch-1.yaml": `apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +`, + "foo.patch-2.yaml": `apiVersion: v1 +kind: Foo +metadata: + name: foo2 +spec: something +`, + "bar.patch-1.yaml": `apiVersion: v1 +kind: Bar +metadata: + name: bar1 +spec: something +`, + "bar.patch-2.yaml": `apiVersion: v1 +kind: Bar +metadata: + name: bar2 +spec: something +`, + }, + }, + + "split multiple patches into multiple patches (mix of files and inline)": { + kustomization: Kustomization{ + PatchesStrategicMerge: []PatchStrategicMerge{"secret.patch.yaml", "foo.patch.yaml", ` +--- # Some comment +apiVersion: v1 +kind: Bar +metadata: + name: bar1 +spec: something +--- +--- +apiVersion: v1 +kind: Bar +metadata: + name: bar2 +spec: something +--- +`}, + }, + files: map[string]string{ + "secret.patch.yaml": ` +# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword + +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "foo.patch.yaml": ` +apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo2 +spec: something +`, + }, + expectedKustomization: Kustomization{ + Patches: []Patch{ + {Path: "secret.patch-1.yaml"}, + {Path: "secret.patch-2.yaml"}, + {Path: "foo.patch-1.yaml"}, + {Path: "foo.patch-2.yaml"}, + {Patch: `apiVersion: v1 +kind: Bar +metadata: + name: bar1 +spec: something +`}, + {Patch: `apiVersion: v1 +kind: Bar +metadata: + name: bar2 +spec: something +`}, + }, + }, + expectedFiles: map[string]string{ + "secret.patch-1.yaml": `# secret.patch.yaml +apiVersion: v1 +kind: Secret +metadata: + name: secret1 +type: Opaque +stringData: + admin.password: newpassword +`, + "secret.patch-2.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: secret2 +type: Opaque +stringData: + admin.user: newuser +`, + "foo.patch-1.yaml": `apiVersion: v1 +kind: Foo +metadata: + name: foo1 +spec: something +`, + "foo.patch-2.yaml": `apiVersion: v1 +kind: Foo +metadata: + name: foo2 +spec: something +`, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + fSys := filesys.MakeFsInMemory() + for filename, content := range tc.files { + require.NoError(t, fSys.WriteFile(filename, []byte(content))) + } + + err := tc.kustomization.FixKustomizationPreMarshalling(fSys) + if err != nil { + t.Fatal(err) + } + + require.Emptyf(t, cmp.Diff(tc.expectedKustomization, tc.kustomization), "kustomization mismatch") + + for filename, expectedContent := range tc.expectedFiles { + actualContent, err := fSys.ReadFile(filename) + if err != nil { + t.Fatalf("failed to read expected file %s: %v", filename, err) + } + require.Emptyf(t, cmp.Diff(expectedContent, string(actualContent)), "file %s content mismatch", filename) + } + + for filename := range tc.files { + if _, ok := tc.expectedFiles[filename]; ok { + continue + } + if fSys.Exists(filename) { + t.Errorf("expected file %s to have been deleted", filename) + } + } + }) + } +} diff --git a/kyaml/kio/byteio_reader.go b/kyaml/kio/byteio_reader.go index 230ab891bbb..86c99b61963 100644 --- a/kyaml/kio/byteio_reader.go +++ b/kyaml/kio/byteio_reader.go @@ -164,15 +164,13 @@ type ByteReader struct { var _ Reader = &ByteReader{} -// splitDocuments returns a slice of all documents contained in a YAML string. Multiple documents can be divided by the +// SplitDocuments returns a slice of all documents contained in a YAML string. Multiple documents can be divided by the // YAML document separator (---). It allows for white space and comments to be after the separator on the same line, // but will return an error if anything else is on the line. -func splitDocuments(s string) ([]string, error) { +func SplitDocuments(s string) ([]string, error) { docs := make([]string, 0) if len(s) > 0 { - // The YAML document separator is any line that starts with --- - yamlSeparatorRegexp := regexp.MustCompile(`\n---.*\n`) - + yamlSeparatorRegexp := regexp.MustCompile("(?m)^---.*$") // Matches any line that starts with --- // Find all separators, check them for invalid content, and append each document to docs separatorLocations := yamlSeparatorRegexp.FindAllStringIndex(s, -1) prev := 0 @@ -181,9 +179,12 @@ func splitDocuments(s string) ([]string, error) { separator := s[loc[0]:loc[1]] // If the next non-whitespace character on the line following the separator is not a comment, return an error - trimmedContentAfterSeparator := strings.TrimSpace(separator[4:]) - if len(trimmedContentAfterSeparator) > 0 && trimmedContentAfterSeparator[0] != '#' { - return nil, errors.Errorf("invalid document separator: %s", strings.TrimSpace(separator)) + const yamlSeparatorLength = 3 + if len(separator) > yamlSeparatorLength { + trimmedContentAfterSeparator := strings.TrimSpace(separator[yamlSeparatorLength:]) + if len(trimmedContentAfterSeparator) > 0 && trimmedContentAfterSeparator[0] != '#' { + return nil, errors.Errorf("invalid document separator: %s", strings.TrimSpace(separator)) + } } docs = append(docs, s[prev:loc[0]]) @@ -212,7 +213,7 @@ func (r *ByteReader) Read() ([]*yaml.RNode, error) { // Replace the ending \r\n (line ending used in windows) with \n and then split it into multiple YAML documents // if it contains document separators (---) - values, err := splitDocuments(strings.ReplaceAll(input.String(), "\r\n", "\n")) + values, err := SplitDocuments(strings.ReplaceAll(input.String(), "\r\n", "\n")) if err != nil { return nil, errors.Wrap(err) }