Skip to content

Commit

Permalink
fix: budgetadjutments events - handle multiple YAML documents in one …
Browse files Browse the repository at this point in the history
…file (#294)

## Motivation

Currently, only the first document in the input file is processed,
meaning any additional documents are ignored. This change ensures that
all YAML documents within a file are processed correctly.

## Summary

This change updates the processing of YAML input files to send a
separate request for each YAML document found, rather than only sending
the first document.

#Changes

Modified the YAML parsing logic to extract all documents from the input
file.
Each document is now converted to JSON and sent as an individual
request.
No structural validation or merging of documents is performed.

#Justification

Instead of merging multiple YAML documents into a single request body,
each document is now handled separately. Merging them would require
additional logic to determine how different documents should be
combined, which would significantly complicate the code just to
accommodate a niche use case. Additionally, encountering multiple YAML
documents in a single input file is an edge case and highly unlikely to
happen in normal usage. Given that this part of the code is only
responsible for translating YAML to JSON without validating its
structure, keeping the implementation simple and predictable is
preferable.

#Disadvantages 

- Loss of Transactionality: Sending separate requests for each YAML
document means the process is no longer atomic. If one request fails,
previous requests will already be processed, and the system will not
roll back to a consistent state.
- Partial Execution: The process will stop at the first error
encountered, leaving subsequent YAML documents unprocessed. This breaks
the "all or nothing" approach expected in sloctl workflows.

## Testing

Run update and delete events command with a yaml file containing more
then one document. For example:

```
---
---
# commented code
---
- eventStart: 2024-12-04T06:37:00Z
  eventEnd: 2024-12-04T06:59:00Z
  slos:
    - project: test-project
      name: sample-slo-1-578b974d-8e27-43cf-85a3-7751a774f13d
  update:
    eventStart: 2024-12-04T06:37:00Z
    eventEnd: 2024-12-04T06:59:00Z
---
grocery_list:
  - category: Fruits
    items:
      - name: Apples
        quantity: 4
      - name: Bananas
        quantity: 6
      - name: Oranges
        quantity: 3

  - category: Vegetables
    items:
      - name: Carrots
        quantity: 2
      - name: Broccoli
        quantity: 1
      - name: Spinach
        quantity: 1 bunch
---
---
```

## Release Notes

The `budgetadjustments events` commands now processes all YAML documents
in an input file instead of only the first one, ensuring no data is
missed. Each document is sent as a separate request to the API
  • Loading branch information
piotrkazulak authored Mar 4, 2025
1 parent 1189b2e commit efe5e93
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 32 deletions.
2 changes: 1 addition & 1 deletion internal/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.1
0.11.3
32 changes: 17 additions & 15 deletions internal/budgetadjustments/events/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,26 @@ func NewDeleteCmd(clientProvider sdkclient.SdkClientProvider) *cobra.Command {
}

func (g *DeleteCmd) run(cmd *cobra.Command) error {
data, err := readFile(g.filepath)
docs, err := getEventsStringsFromFile(g.filepath)
if err != nil {
return errors.Wrap(err, "failed to read input data")
}
body, err := yaml.YAMLToJSON(data)
if err != nil {
return errors.Wrap(err, "failed to convert input data to JSON")
return errors.Wrap(err, "failed to read events form file")
}

if _, err = DoRequest(
g.client,
cmd.Context(),
http.MethodPost,
fmt.Sprintf("%s/%s/events/delete", BudgetAdjustmentAPI, g.adjustment),
nil,
bytes.NewReader(body),
); err != nil {
return err
for _, doc := range docs {
jsonBytes, err := yaml.YAMLToJSON([]byte(doc))
if err != nil {
return errors.Wrap(err, "failed to convert input data to JSON")
}
if _, err = DoRequest(
g.client,
cmd.Context(),
http.MethodPost,
fmt.Sprintf("%s/%s/events/delete", BudgetAdjustmentAPI, g.adjustment),
nil,
bytes.NewReader(jsonBytes),
); err != nil {
return err
}
}

return nil
Expand Down
24 changes: 24 additions & 0 deletions internal/budgetadjustments/events/inputreader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"io"
"os"
"path/filepath"
"regexp"

"github.com/pkg/errors"
)

func readFile(path string) (data []byte, err error) {
Expand All @@ -13,3 +16,24 @@ func readFile(path string) (data []byte, err error) {
path = filepath.Clean(path)
return os.ReadFile(path) // #nosec G304
}

func getEventsStringsFromFile(path string) ([]string, error) {
data, err := readFile(path)
if err != nil {
return nil, errors.Wrap(err, "failed to read input data")
}
return splitYAMLDocs(data), nil
}

func splitYAMLDocs(data []byte) []string {
re := regexp.MustCompile("(?m)^---$\n?")
split := re.Split(string(data), -1)
docs := make([]string, 0, len(split))
for _, docStr := range split {
if len(docStr) < 1 {
continue
}
docs = append(docs, docStr)
}
return docs
}
160 changes: 160 additions & 0 deletions internal/budgetadjustments/events/inputreader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package events

import (
"reflect"
"strings"
"testing"
)

func TestSplitYAMLDocuments(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "Basic YAML Split",
input: `---
name: doc1
value: 1
---
name: doc2
value: 2
---
name: doc3
value: 3
`,
expected: []string{
"name: doc1\nvalue: 1",
"name: doc2\nvalue: 2",
"name: doc3\nvalue: 3",
},
},
{
name: "Basic YAML Split with additional separators",
input: `---
---
---
name: doc1
value: 1
---
---
name: doc2
value: 2
---
name: doc3
value: 3
---
---`,
expected: []string{
"name: doc1\nvalue: 1",
"name: doc2\nvalue: 2",
"name: doc3\nvalue: 3",
},
},
{
name: "YAML with Lists",
input: `---
list:
- item1
- item2
---
list:
- item3
- item4
`,
expected: []string{
"list:\n - item1\n - item2",
"list:\n - item3\n - item4",
},
},
{
name: "YAML with Nested Structures",
input: `---
parent:
child: value1
---
parent:
child: value2
`,
expected: []string{
"parent:\n child: value1",
"parent:\n child: value2",
},
},
{
name: "invalid YAML",
input: `---
foo bar
baz: bob
`,
expected: []string{"foo bar\nbaz: bob"},
},
{
name: "just YAML",
input: "YAML",
expected: []string{"YAML"},
},
{
name: "YAML with doc separators found in content",
input: `---
parent:
child: "foo---bar"
---
parent:
child: value2
---
----nt:
child: value3
----
---
---
---
`,
expected: []string{
"parent:\n child: \"foo---bar\"",
"parent:\n child: value2",
"----nt:\n child: value3\n----",
"---\n ---",
},
},
{
name: "YAML with correct event format",
input: `
- eventStart: 2024-12-04T06:37:00Z
eventEnd: 2024-12-04T06:59:00Z
slos:
- project: test-project
name: sample-slo-1-578b974d-8e27-43cf-85a3-7751a774f13d
update:
eventStart: 2024-12-04T06:37:00Z
eventEnd: 2024-12-04T06:59:00Z
`,
expected: []string{
`- eventStart: 2024-12-04T06:37:00Z
eventEnd: 2024-12-04T06:59:00Z
slos:
- project: test-project
name: sample-slo-1-578b974d-8e27-43cf-85a3-7751a774f13d
update:
eventStart: 2024-12-04T06:37:00Z
eventEnd: 2024-12-04T06:59:00Z`,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := splitYAMLDocs([]byte(tt.input))

// Trim whitespace from results for better comparison
for i := range result {
result[i] = strings.TrimSpace(result[i])
}

if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("Test %s failed. Expected %v, but got %v", tt.name, tt.expected, result)
}
})
}
}
34 changes: 18 additions & 16 deletions internal/budgetadjustments/events/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,27 @@ func NewUpdateCmd(clientProvider sdkclient.SdkClientProvider) *cobra.Command {
}

func (g *UpdateCmd) run(cmd *cobra.Command) error {
data, err := readFile(g.filepath)
docs, err := getEventsStringsFromFile(g.filepath)
if err != nil {
return errors.Wrap(err, "failed to read input data")
}
body, err := yaml.YAMLToJSON(data)
if err != nil {
return errors.Wrap(err, "failed to convert input data to JSON")
return errors.Wrap(err, "failed to read events form file")
}

_, err = DoRequest(
g.client,
cmd.Context(),
http.MethodPut,
fmt.Sprintf("%s/%s/events/update", BudgetAdjustmentAPI, g.adjustment),
nil,
bytes.NewReader(body),
)
if err != nil {
return err
for _, doc := range docs {
jsonBytes, err := yaml.YAMLToJSON([]byte(doc))
if err != nil {
return errors.Wrap(err, "failed to convert input data to JSON")
}
_, err = DoRequest(
g.client,
cmd.Context(),
http.MethodPut,
fmt.Sprintf("%s/%s/events/update", BudgetAdjustmentAPI, g.adjustment),
nil,
bytes.NewReader(jsonBytes),
)
if err != nil {
return err
}
}

return nil
Expand Down

0 comments on commit efe5e93

Please sign in to comment.